Skip to main content

wlancfg_lib/telemetry/
mod.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5mod convert;
6mod windowed_stats;
7
8use crate::client;
9
10use crate::client::roaming::lib::{PolicyRoamRequest, RoamReason};
11use crate::client::state_machine::TrackedSignals;
12use crate::mode_management::{Defect, IfaceFailure};
13use crate::telemetry::windowed_stats::WindowedStats;
14use crate::util::pseudo_energy::{EwmaSignalData, RssiVelocity};
15use anyhow::{Context, Error, format_err};
16use cobalt_client::traits::AsEventCode;
17use fidl_fuchsia_metrics::{MetricEvent, MetricEventPayload};
18use fidl_fuchsia_wlan_common as fidl_common;
19use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
20use fidl_fuchsia_wlan_internal as fidl_internal;
21use fidl_fuchsia_wlan_sme as fidl_sme;
22use fuchsia_async::{self as fasync, TimeoutExt};
23use fuchsia_inspect::{
24    ArrayProperty, InspectType, Inspector, LazyNode, Node as InspectNode, NumericProperty,
25    Property, UintProperty,
26};
27use fuchsia_inspect_contrib::inspectable::{InspectableBool, InspectableU64};
28use fuchsia_inspect_contrib::log::{InspectBytes, InspectList};
29use fuchsia_inspect_contrib::nodes::BoundedListNode;
30use fuchsia_inspect_contrib::{inspect_insert, inspect_log, make_inspect_loggable};
31use fuchsia_sync::Mutex;
32use futures::channel::{mpsc, oneshot};
33use futures::{Future, FutureExt, StreamExt, select};
34use log::{error, info, warn};
35use num_traits::SaturatingAdd;
36use static_assertions::const_assert_eq;
37use std::cmp::{Reverse, max, min};
38use std::collections::{HashMap, HashSet};
39use std::ops::Add;
40use std::sync::atomic::{AtomicBool, Ordering};
41use std::sync::{Arc, Once};
42use wlan_common::channel::{Bandwidth, Channel};
43use wlan_common::historical_list::{HistoricalList, Timestamped};
44use wlan_metrics_registry as metrics;
45use wlan_telemetry::{ThrottledErrorLogger, TimeoutSource};
46
47// Include a timeout on stats calls so that if the driver deadlocks, telemtry doesn't get stuck.
48const GET_IFACE_STATS_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(5);
49
50// Short duration connection for metrics purposes.
51pub const METRICS_SHORT_CONNECT_DURATION: zx::MonotonicDuration =
52    zx::MonotonicDuration::from_seconds(90);
53// Minimum connection duration for logging average connection score deltas.
54pub const AVERAGE_SCORE_DELTA_MINIMUM_DURATION: zx::MonotonicDuration =
55    zx::MonotonicDuration::from_seconds(30);
56// Maximum value of reason code accepted by cobalt metrics (set by max_event_code)
57pub const COBALT_REASON_CODE_MAX: u16 = 1000;
58// Time between cobalt error reports to prevent cluttering up the syslog.
59pub const MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS: i64 = 60;
60/// Number of previous RSSI measurements to exponentially weigh into average.
61/// TODO(https://fxbug.dev/42165706): Tune smoothing factor.
62pub const EWMA_SMOOTHING_FACTOR_FOR_METRICS: usize = 10;
63
64#[derive(Clone, Debug, PartialEq)]
65// Connection score and the time at which it was calculated.
66pub struct TimestampedConnectionScore {
67    pub score: u8,
68    pub time: fasync::MonotonicInstant,
69}
70impl TimestampedConnectionScore {
71    pub fn new(score: u8, time: fasync::MonotonicInstant) -> Self {
72        Self { score, time }
73    }
74}
75impl Timestamped for TimestampedConnectionScore {
76    fn time(&self) -> fasync::MonotonicInstant {
77        self.time
78    }
79}
80
81#[derive(Clone)]
82#[cfg_attr(test, derive(Debug))]
83pub struct TelemetrySender {
84    sender: Arc<Mutex<mpsc::Sender<TelemetryEvent>>>,
85    sender_is_blocked: Arc<AtomicBool>,
86}
87
88impl TelemetrySender {
89    pub fn new(sender: mpsc::Sender<TelemetryEvent>) -> Self {
90        Self {
91            sender: Arc::new(Mutex::new(sender)),
92            sender_is_blocked: Arc::new(AtomicBool::new(false)),
93        }
94    }
95
96    // Send telemetry event. Log an error if it fails
97    pub fn send(&self, event: TelemetryEvent) {
98        match self.sender.lock().try_send(event) {
99            Ok(_) => {
100                // If sender has been blocked before, set bool to false and log message
101                if self
102                    .sender_is_blocked
103                    .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
104                    .is_ok()
105                {
106                    info!("TelemetrySender recovered and resumed sending");
107                }
108            }
109            Err(_) => {
110                // If sender has not been blocked before, set bool to true and log error message
111                if self
112                    .sender_is_blocked
113                    .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
114                    .is_ok()
115                {
116                    warn!(
117                        "TelemetrySender dropped a msg: either buffer is full or no receiver is waiting"
118                    );
119                }
120            }
121        }
122    }
123}
124
125#[derive(Clone, Debug, PartialEq)]
126pub struct DisconnectInfo {
127    pub iface_id: u16,
128    pub connected_duration: zx::MonotonicDuration,
129    pub is_sme_reconnecting: bool,
130    pub disconnect_source: fidl_sme::DisconnectSource,
131    pub previous_connect_reason: client::types::ConnectReason,
132    pub ap_state: client::types::ApState,
133    pub signals: TrackedSignals,
134}
135
136pub trait DisconnectSourceExt {
137    fn inspect_string(&self) -> String;
138    fn flattened_reason_code(&self) -> u32;
139    fn cobalt_reason_code(&self) -> u16;
140    fn locally_initiated(&self) -> bool;
141    fn has_roaming_cause(&self) -> bool;
142}
143
144impl DisconnectSourceExt for fidl_sme::DisconnectSource {
145    fn inspect_string(&self) -> String {
146        match self {
147            fidl_sme::DisconnectSource::User(reason) => {
148                format!("source: user, reason: {reason:?}")
149            }
150            fidl_sme::DisconnectSource::Ap(cause) => format!(
151                "source: ap, reason: {:?}, mlme_event_name: {:?}",
152                cause.reason_code, cause.mlme_event_name
153            ),
154            fidl_sme::DisconnectSource::Mlme(cause) => format!(
155                "source: mlme, reason: {:?}, mlme_event_name: {:?}",
156                cause.reason_code, cause.mlme_event_name
157            ),
158        }
159    }
160
161    /// If disconnect comes from AP, then get the 802.11 reason code.
162    /// If disconnect comes from MLME, return (1u32 << 17) + reason code.
163    /// If disconnect comes from user, return (1u32 << 16) + user disconnect reason.
164    /// This is mainly used for metric.
165    fn flattened_reason_code(&self) -> u32 {
166        match self {
167            fidl_sme::DisconnectSource::Ap(cause) => cause.reason_code.into_primitive() as u32,
168            fidl_sme::DisconnectSource::User(reason) => (1u32 << 16) + *reason as u32,
169            fidl_sme::DisconnectSource::Mlme(cause) => {
170                (1u32 << 17) + (cause.reason_code.into_primitive() as u32)
171            }
172        }
173    }
174
175    fn cobalt_reason_code(&self) -> u16 {
176        match self {
177            // Cobalt metrics expects reason_code value to be less than COBALT_REASON_CODE_MAX.
178            fidl_sme::DisconnectSource::Ap(cause) => {
179                std::cmp::min(cause.reason_code.into_primitive(), COBALT_REASON_CODE_MAX)
180            }
181            fidl_sme::DisconnectSource::User(reason) => {
182                std::cmp::min(*reason as u16, COBALT_REASON_CODE_MAX)
183            }
184            fidl_sme::DisconnectSource::Mlme(cause) => {
185                std::cmp::min(cause.reason_code.into_primitive(), COBALT_REASON_CODE_MAX)
186            }
187        }
188    }
189
190    fn locally_initiated(&self) -> bool {
191        match self {
192            fidl_sme::DisconnectSource::Ap(..) => false,
193            fidl_sme::DisconnectSource::Mlme(..) | fidl_sme::DisconnectSource::User(..) => true,
194        }
195    }
196
197    fn has_roaming_cause(&self) -> bool {
198        match self {
199            fidl_sme::DisconnectSource::User(_) => false,
200            fidl_sme::DisconnectSource::Ap(cause) | fidl_sme::DisconnectSource::Mlme(cause) => {
201                matches!(
202                    cause.mlme_event_name,
203                    fidl_sme::DisconnectMlmeEventName::RoamStartIndication
204                        | fidl_sme::DisconnectMlmeEventName::RoamResultIndication
205                        | fidl_sme::DisconnectMlmeEventName::RoamRequest
206                        | fidl_sme::DisconnectMlmeEventName::RoamConfirmation
207                )
208            }
209        }
210    }
211}
212
213#[derive(Debug, PartialEq)]
214pub struct ScanEventInspectData {
215    pub unknown_protection_ies: Vec<String>,
216}
217
218impl Default for ScanEventInspectData {
219    fn default() -> Self {
220        Self::new()
221    }
222}
223
224impl ScanEventInspectData {
225    pub fn new() -> Self {
226        Self { unknown_protection_ies: vec![] }
227    }
228}
229
230#[cfg_attr(test, derive(Debug))]
231pub enum TelemetryEvent {
232    /// Request telemetry for the latest status
233    QueryStatus {
234        sender: oneshot::Sender<QueryStatusResult>,
235    },
236    /// Notify the telemetry event loop that the process of establishing connection is started
237    StartEstablishConnection {
238        /// If set to true, use the current time as the start time of the establish connection
239        /// process. If set to false, then use the start time initialized from the previous
240        /// StartEstablishConnection event, or use the current time if there isn't an existing
241        /// start time.
242        reset_start_time: bool,
243    },
244    /// Clear any existing start time of establish connection process tracked by telemetry.
245    ClearEstablishConnectionStartTime,
246    /// Notify the telemetry event loop of an active scan being requested.
247    ActiveScanRequested {
248        num_ssids_requested: usize,
249    },
250    /// Notify the telemetry event loop of an active scan being requested via Policy API.
251    ActiveScanRequestedViaApi {
252        num_ssids_requested: usize,
253    },
254    /// Notify the telemetry event loop that network selection is complete.
255    NetworkSelectionDecision {
256        /// Type of network selection. If it's undirected and no candidate network is found,
257        /// telemetry will toggle the "no saved neighbor" flag.
258        network_selection_type: NetworkSelectionType,
259        /// When there's a scan error, `num_candidates` should be Err.
260        /// When `num_candidates` is `Ok(0)` for an undirected network selection, telemetry
261        /// will toggle the "no saved neighbor" flag.  If the event loop is tracking downtime,
262        /// the subsequent downtime period will also be used to increment the,
263        /// `downtime_no_saved_neighbor_duration` counter. This counter is used to
264        /// adjust the raw downtime.
265        num_candidates: Result<usize, ()>,
266        /// Count of number of networks selected. This will be 0 if there are no candidates selected
267        /// including if num_candidates is Ok(0) or Err. However, this will only be logged to
268        /// Cobalt is num_candidates is not Err and is greater than 0.
269        selected_count: usize,
270    },
271    /// Notify the telemetry event loop of connection result.
272    /// If connection result is successful, telemetry will move its internal state to
273    /// connected. Subsequently, the telemetry event loop will increment the `connected_duration`
274    /// counter periodically.
275    ConnectResult {
276        iface_id: u16,
277        policy_connect_reason: Option<client::types::ConnectReason>,
278        result: fidl_sme::ConnectResult,
279        multiple_bss_candidates: bool,
280        ap_state: client::types::ApState,
281        network_is_likely_hidden: bool,
282    },
283    /// Notify the telemetry event loop of roam result.
284    /// If roam result is unsuccessful, telemetry will move its internal state to
285    /// disconnected.
286    PolicyInitiatedRoamResult {
287        iface_id: u16,
288        result: fidl_sme::RoamResult,
289        updated_ap_state: client::types::ApState,
290        original_ap_state: Box<client::types::ApState>,
291        request: Box<PolicyRoamRequest>,
292        request_time: fasync::MonotonicInstant,
293        result_time: fasync::MonotonicInstant,
294    },
295    /// Notify the telemetry event loop that the client has disconnected.
296    /// Subsequently, the telemetry event loop will increment the downtime counters periodically
297    /// if TelemetrySender has requested downtime to be tracked via `track_subsequent_downtime`
298    /// flag.
299    Disconnected {
300        /// Indicates whether subsequent period should be used to increment the downtime counters.
301        track_subsequent_downtime: bool,
302        info: Option<DisconnectInfo>,
303    },
304    OnSignalReport {
305        ind: fidl_internal::SignalReportIndication,
306    },
307    OnSignalVelocityUpdate {
308        rssi_velocity: f64,
309    },
310    OnChannelSwitched {
311        info: fidl_internal::ChannelSwitchInfo,
312    },
313    /// Notify telemetry that there was a decision to look for networks to roam to after evaluating
314    /// the existing connection.
315    PolicyRoamScan {
316        reasons: Vec<RoamReason>,
317    },
318    /// Notify telemetry that the roam monitor has decided to attempt a roam to a candidate.
319    PolicyRoamAttempt {
320        request: PolicyRoamRequest,
321        connected_duration: zx::MonotonicDuration,
322    },
323    /// Proactive roams do not happen yet, but we want to analyze metrics for when they would
324    /// happen. Roams are set up to log metrics when disconnects happen to roam, so this event
325    /// covers when roams would happen but no actual disconnect happens.
326    WouldRoamConnect,
327    /// Counts of saved networks and count of configurations for each of those networks, to be
328    /// recorded periodically.
329    SavedNetworkCount {
330        saved_network_count: usize,
331        config_count_per_saved_network: Vec<usize>,
332    },
333    /// Record the time since the last network selection scan
334    NetworkSelectionScanInterval {
335        time_since_last_scan: zx::MonotonicDuration,
336    },
337    /// Statistics about networks observed in scan results for Connection Selection
338    ConnectionSelectionScanResults {
339        saved_network_count: usize,
340        bss_count_per_saved_network: Vec<usize>,
341        saved_network_count_found_by_active_scan: usize,
342    },
343    PostConnectionSignals {
344        connect_time: fasync::MonotonicInstant,
345        signal_at_connect: client::types::Signal,
346        signals: TrackedSignals,
347    },
348    /// Notify telemetry of an API request to start client connections.
349    StartClientConnectionsRequest,
350    /// Notify telemetry of an API request to stop client connections.
351    StopClientConnectionsRequest,
352    /// Notify telemetry of when AP is stopped, and how long it was started.
353    StopAp {
354        enabled_duration: zx::MonotonicDuration,
355    },
356    /// Notify telemetry of the result of a create iface request.
357    IfaceCreationResult {
358        role: fidl_common::WlanMacRole,
359        result: Result<u16, ()>,
360    },
361    /// Notify telemetry of the result of destroying an interface.
362    IfaceDestructionResult {
363        role: fidl_common::WlanMacRole,
364        result: Result<u16, ()>,
365    },
366    /// Notify telemetry of the result of a StartAp request.
367    StartApResult(Result<(), ()>),
368    /// Record scan fulfillment time
369    ScanRequestFulfillmentTime {
370        duration: zx::MonotonicDuration,
371        reason: client::scan::ScanReason,
372    },
373    /// Record scan queue length upon scan completion
374    ScanQueueStatistics {
375        fulfilled_requests: usize,
376        remaining_requests: usize,
377    },
378    /// Record the results of a completed BSS selection
379    BssSelectionResult {
380        reason: client::types::ConnectReason,
381        scored_candidates: Vec<(client::types::ScannedCandidate, i16)>,
382        selected_candidate: Option<(client::types::ScannedCandidate, i16)>,
383    },
384    ScanEvent {
385        inspect_data: ScanEventInspectData,
386        scan_defects: Vec<ScanIssue>,
387    },
388    LongDurationSignals {
389        signals: Vec<client::types::TimestampedSignal>,
390    },
391    /// Record recovery events and store recovery-related metadata so that the
392    /// efficacy of the recovery mechanism can be evaluated later.
393    RecoveryEvent {
394        reason: RecoveryReason,
395    },
396    SmeTimeout {
397        source: TimeoutSource,
398    },
399    SmeScanStart,
400    SmeScanResult {
401        result: wlan_telemetry::ScanResult,
402    },
403    ChipPowerUpFailure,
404}
405
406#[derive(Clone, Debug)]
407pub struct QueryStatusResult {
408    connection_state: ConnectionStateInfo,
409}
410
411#[derive(Clone, Debug)]
412pub enum ConnectionStateInfo {
413    Idle,
414    Disconnected,
415    Connected {
416        iface_id: u16,
417        ap_state: Box<client::types::ApState>,
418        telemetry_proxy: Option<fidl_fuchsia_wlan_sme::TelemetryProxy>,
419    },
420}
421
422#[derive(Clone, Debug, PartialEq)]
423pub enum NetworkSelectionType {
424    /// Looking for the best BSS from any saved networks
425    Undirected,
426    /// Looking for the best BSS for a particular network
427    Directed,
428}
429
430#[derive(Debug, PartialEq)]
431pub enum ScanIssue {
432    ScanFailure,
433    AbortedScan,
434    EmptyScanResults,
435}
436
437impl ScanIssue {
438    fn as_metric_id(&self) -> u32 {
439        match self {
440            ScanIssue::ScanFailure => metrics::CLIENT_SCAN_FAILURE_METRIC_ID,
441            ScanIssue::AbortedScan => metrics::ABORTED_SCAN_METRIC_ID,
442            ScanIssue::EmptyScanResults => metrics::EMPTY_SCAN_RESULTS_METRIC_ID,
443        }
444    }
445}
446
447pub type ClientRecoveryMechanism = metrics::ConnectivityWlanMetricDimensionClientRecoveryMechanism;
448pub type ApRecoveryMechanism = metrics::ConnectivityWlanMetricDimensionApRecoveryMechanism;
449pub type TimeoutRecoveryMechanism =
450    metrics::ConnectivityWlanMetricDimensionTimeoutRecoveryMechanism;
451
452#[derive(Copy, Clone, Debug, PartialEq)]
453pub enum PhyRecoveryMechanism {
454    PhyReset = 0,
455}
456
457#[derive(Copy, Clone, Debug, PartialEq)]
458pub enum RecoveryReason {
459    CreateIfaceFailure(PhyRecoveryMechanism),
460    DestroyIfaceFailure(PhyRecoveryMechanism),
461    Timeout(TimeoutRecoveryMechanism),
462    ConnectFailure(ClientRecoveryMechanism),
463    StartApFailure(ApRecoveryMechanism),
464    ScanFailure(ClientRecoveryMechanism),
465    ScanCancellation(ClientRecoveryMechanism),
466    ScanResultsEmpty(ClientRecoveryMechanism),
467}
468
469struct RecoveryRecord {
470    scan_failure: Option<RecoveryReason>,
471    scan_cancellation: Option<RecoveryReason>,
472    scan_results_empty: Option<RecoveryReason>,
473    connect_failure: Option<RecoveryReason>,
474    start_ap_failure: Option<RecoveryReason>,
475    create_iface_failure: Option<RecoveryReason>,
476    destroy_iface_failure: Option<RecoveryReason>,
477    timeout: Option<RecoveryReason>,
478}
479
480impl RecoveryRecord {
481    fn new() -> Self {
482        RecoveryRecord {
483            scan_failure: None,
484            scan_cancellation: None,
485            scan_results_empty: None,
486            connect_failure: None,
487            start_ap_failure: None,
488            create_iface_failure: None,
489            destroy_iface_failure: None,
490            timeout: None,
491        }
492    }
493
494    fn record_recovery_attempt(&mut self, reason: RecoveryReason) {
495        match reason {
496            RecoveryReason::ScanFailure(_) => self.scan_failure = Some(reason),
497            RecoveryReason::ScanCancellation(_) => self.scan_cancellation = Some(reason),
498            RecoveryReason::ScanResultsEmpty(_) => self.scan_results_empty = Some(reason),
499            RecoveryReason::ConnectFailure(_) => self.connect_failure = Some(reason),
500            RecoveryReason::StartApFailure(_) => self.start_ap_failure = Some(reason),
501            RecoveryReason::CreateIfaceFailure(_) => self.create_iface_failure = Some(reason),
502            RecoveryReason::DestroyIfaceFailure(_) => self.destroy_iface_failure = Some(reason),
503            RecoveryReason::Timeout(_) => self.timeout = Some(reason),
504        }
505    }
506}
507
508pub type RecoveryOutcome = metrics::ConnectivityWlanMetricDimensionResult;
509
510/// Capacity of "first come, first serve" slots available to clients of
511/// the mpsc::Sender<TelemetryEvent>.
512const TELEMETRY_EVENT_BUFFER_SIZE: usize = 100;
513/// How often to request RSSI stats and dispatcher packet counts from MLME.
514const TELEMETRY_QUERY_INTERVAL: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(15);
515
516pub fn get_telemetry_config() -> wlan_telemetry::TelemetryConfig {
517    wlan_telemetry::TelemetryConfig {
518        enable_connect_disconnect: true,
519        enable_iface_logger: true,
520        enable_sme_timeout_logger: true,
521        enable_toggle_logger: true,
522        enable_recovery_logger: true,
523        enable_client_iface_counters_logger: true,
524        enable_scan_logger: true,
525        device_mobility: wlan_telemetry::DeviceMobility::Stationary,
526        ..Default::default()
527    }
528}
529
530pub fn get_cobalt_allowlist() -> wlan_telemetry::CobaltAllowlist {
531    wlan_telemetry::CobaltAllowlist::Only(HashSet::from([
532        metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
533        metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
534        metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID,
535        metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID,
536        metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID,
537        metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID,
538        metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
539        metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
540        metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID,
541        metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
542        metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
543        metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
544        metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID,
545        metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
546        metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID,
547        metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID,
548        metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID,
549        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
550        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
551        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
552        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_RSSI_BUCKET_METRIC_ID,
553        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SNR_BUCKET_METRIC_ID,
554        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
555        metrics::CLIENT_CONNECTION_ENABLED_OCCURRENCE_METRIC_ID,
556        metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID,
557        metrics::CLIENT_CONNECTION_ENABLED_DURATION_METRIC_ID,
558        metrics::RECOVERY_OCCURRENCE_2_METRIC_ID,
559        metrics::GET_IFACE_STATS_FAILURE_METRIC_ID,
560        metrics::GET_IFACE_STATS_TIMEOUT_METRIC_ID,
561        metrics::GET_IFACE_STATS_ERROR_IN_RESPONSE_METRIC_ID,
562        metrics::BAD_RX_RATE_METRIC_ID,
563        metrics::RX_UNICAST_PACKETS_METRIC_ID,
564        metrics::BAD_TX_RATE_METRIC_ID,
565        metrics::INTERFACE_CREATION_FAILURE_METRIC_ID,
566        metrics::INTERFACE_DESTRUCTION_FAILURE_METRIC_ID,
567        metrics::SME_OPERATION_TIMEOUT_METRIC_ID,
568        metrics::SME_OPERATION_TIMEOUT_2_METRIC_ID,
569        metrics::SCAN_OCCURRENCE_METRIC_ID,
570        metrics::SCAN_FULFILLMENT_TIME_METRIC_ID,
571        metrics::EMPTY_SCAN_RESULTS_METRIC_ID,
572        metrics::CLIENT_SCAN_FAILURE_METRIC_ID,
573        metrics::ABORTED_SCAN_METRIC_ID,
574    ]))
575}
576
577/// Create a struct for sending TelemetryEvent, and a future representing the telemetry loop.
578///
579/// Every 15 seconds, the telemetry loop will query for MLME/PHY stats and update various
580/// time-interval stats. The telemetry loop also handles incoming TelemetryEvent to update
581/// the appropriate stats.
582pub fn serve_telemetry(
583    monitor_svc_proxy: fidl_fuchsia_wlan_device_service::DeviceMonitorProxy,
584    cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
585    inspect_node: InspectNode,
586    external_inspect_node: InspectNode,
587    defect_sender: mpsc::Sender<Defect>,
588) -> (TelemetrySender, impl Future<Output = ()>) {
589    let (sender, mut receiver) = mpsc::channel::<TelemetryEvent>(TELEMETRY_EVENT_BUFFER_SIZE);
590    let sender = TelemetrySender::new(sender);
591    let cloned_sender = sender.clone();
592    let fut = async move {
593        let mut report_interval_stream = fasync::Interval::new(TELEMETRY_QUERY_INTERVAL);
594        const ONE_MINUTE: zx::MonotonicDuration = zx::MonotonicDuration::from_minutes(1);
595        const_assert_eq!(ONE_MINUTE.into_nanos() % TELEMETRY_QUERY_INTERVAL.into_nanos(), 0);
596        const INTERVAL_TICKS_PER_MINUTE: u64 =
597            (ONE_MINUTE.into_nanos() / TELEMETRY_QUERY_INTERVAL.into_nanos()) as u64;
598        const INTERVAL_TICKS_PER_HR: u64 = INTERVAL_TICKS_PER_MINUTE * 60;
599        const INTERVAL_TICKS_PER_DAY: u64 = INTERVAL_TICKS_PER_HR * 24;
600        let mut interval_tick = 0u64;
601        let lib_telemetry_node = inspect_node.create_child("lib_telemetry");
602        let (lib_telemetry_sender, lib_telemetry_fut) = wlan_telemetry::serve_telemetry(
603            cobalt_proxy.clone(),
604            monitor_svc_proxy.clone(),
605            lib_telemetry_node,
606            "root/client_stats/lib_telemetry",
607            get_telemetry_config(),
608            get_cobalt_allowlist(),
609        );
610        let mut lib_telemetry_fut = Box::pin(lib_telemetry_fut).fuse();
611        let mut telemetry = Telemetry::new(
612            cloned_sender,
613            monitor_svc_proxy,
614            cobalt_proxy,
615            inspect_node,
616            external_inspect_node,
617            defect_sender.clone(),
618        );
619        loop {
620            select! {
621                res = lib_telemetry_fut => {
622                    warn!("wlan_telemetry exited unexpectedly: {:?}", res);
623                }
624                event = receiver.next() => {
625                    if let Some(event) = event {
626                        if let Some(wlan_event) = convert::convert_to_wlan_telemetry_event(&event) {
627                            lib_telemetry_sender.send(wlan_event);
628                        }
629                        telemetry.handle_telemetry_event(event).await;
630                    }
631                }
632                _ = report_interval_stream.next() => {
633                    telemetry.handle_periodic_telemetry().await;
634
635                    interval_tick += 1;
636                    if interval_tick.is_multiple_of(INTERVAL_TICKS_PER_DAY) {
637                        telemetry.log_daily_cobalt_metrics().await;
638                    }
639
640                    // This ensures that `signal_hr_passed` is always called after
641                    // `handle_periodic_telemetry` at the hour mark. This helps with
642                    // ease of testing. Additionally, logging to Cobalt before sliding
643                    // the window ensures that Cobalt uses the last 24 hours of data
644                    // rather than 23 hours.
645                    if interval_tick.is_multiple_of(INTERVAL_TICKS_PER_HR) {
646                        telemetry.signal_hr_passed().await;
647                    }
648                }
649            }
650        }
651    };
652    (sender, fut)
653}
654
655#[derive(Debug)]
656enum ConnectionState {
657    // Like disconnected, but no downtime is tracked.
658    Idle(IdleState),
659    Connected(Box<ConnectedState>),
660    Disconnected(Box<DisconnectedState>),
661}
662
663#[derive(Debug)]
664struct IdleState {
665    connect_start_time: Option<fasync::MonotonicInstant>,
666}
667
668#[derive(Debug)]
669struct ConnectedState {
670    iface_id: u16,
671    /// Time when the user manually initiates connecting to another network via the
672    /// Policy ClientController::Connect FIDL call.
673    new_connect_start_time: Option<fasync::MonotonicInstant>,
674    prev_connection_stats: Option<fidl_fuchsia_wlan_stats::ConnectionStats>,
675    multiple_bss_candidates: bool,
676    ap_state: Box<client::types::ApState>,
677    network_is_likely_hidden: bool,
678
679    last_signal_report: fasync::MonotonicInstant,
680    num_consecutive_get_counter_stats_failures: InspectableU64,
681    is_driver_unresponsive: InspectableBool,
682
683    telemetry_proxy: Option<fidl_fuchsia_wlan_sme::TelemetryProxy>,
684}
685
686#[derive(Debug)]
687pub struct DisconnectedState {
688    disconnected_since: fasync::MonotonicInstant,
689    disconnect_info: Option<DisconnectInfo>,
690    connect_start_time: Option<fasync::MonotonicInstant>,
691    /// The latest time when the device's no saved neighbor duration was accounted.
692    /// If this has a value, then conceptually we say that "no saved neighbor" flag
693    /// is set.
694    latest_no_saved_neighbor_time: Option<fasync::MonotonicInstant>,
695    accounted_no_saved_neighbor_duration: zx::MonotonicDuration,
696}
697
698fn inspect_create_counters(
699    inspect_node: &InspectNode,
700    child_name: &str,
701    counters: Arc<Mutex<WindowedStats<StatCounters>>>,
702) -> LazyNode {
703    inspect_node.create_lazy_child(child_name, move || {
704        let counters = Arc::clone(&counters);
705        async move {
706            let inspector = Inspector::default();
707            {
708                let counters_mutex_guard = counters.lock();
709                let counters = counters_mutex_guard.windowed_stat(None);
710                inspect_insert!(inspector.root(), {
711                    total_duration: counters.total_duration.into_nanos(),
712                    connected_duration: counters.connected_duration.into_nanos(),
713                    downtime_duration: counters.downtime_duration.into_nanos(),
714                    downtime_no_saved_neighbor_duration: counters.downtime_no_saved_neighbor_duration.into_nanos(),
715                    connect_attempts_count: counters.connect_attempts_count,
716                    connect_successful_count: counters.connect_successful_count,
717                    disconnect_count: counters.disconnect_count,
718                    total_non_roam_disconnect_count: counters.total_non_roam_disconnect_count,
719                    total_roam_disconnect_count: counters.total_roam_disconnect_count,
720                    policy_roam_attempts_count: counters.policy_roam_attempts_count,
721                    policy_roam_successful_count: counters.policy_roam_successful_count,
722                    policy_roam_disconnects_count: counters.policy_roam_disconnects_count,
723                    tx_high_packet_drop_duration: counters.tx_high_packet_drop_duration.into_nanos(),
724                    rx_high_packet_drop_duration: counters.rx_high_packet_drop_duration.into_nanos(),
725                    tx_very_high_packet_drop_duration: counters.tx_very_high_packet_drop_duration.into_nanos(),
726                    rx_very_high_packet_drop_duration: counters.rx_very_high_packet_drop_duration.into_nanos(),
727                    no_rx_duration: counters.no_rx_duration.into_nanos(),
728                });
729            }
730            Ok(inspector)
731        }
732        .boxed()
733    })
734}
735
736fn inspect_record_connection_status(inspect_node: &InspectNode, telemetry_sender: TelemetrySender) {
737    inspect_node.record_lazy_child("connection_status", move|| {
738        let telemetry_sender = telemetry_sender.clone();
739        async move {
740            let inspector = Inspector::default();
741            let (sender, receiver) = oneshot::channel();
742            telemetry_sender.send(TelemetryEvent::QueryStatus { sender });
743            let info = match receiver.await {
744                Ok(result) => result.connection_state,
745                Err(e) => {
746                    warn!("Unable to query data for Inspect connection status node: {}", e);
747                    return Ok(inspector)
748                }
749            };
750
751            inspector.root().record_string("status_string", match &info {
752                ConnectionStateInfo::Idle => "idle".to_string(),
753                ConnectionStateInfo::Disconnected => "disconnected".to_string(),
754                ConnectionStateInfo::Connected { .. } => "connected".to_string(),
755            });
756            if let ConnectionStateInfo::Connected { ap_state, .. } = info {
757                inspect_insert!(inspector.root(), connected_network: {
758                    rssi_dbm: ap_state.tracked.signal.rssi_dbm,
759                    snr_db: ap_state.tracked.signal.snr_db,
760                    bssid: ap_state.original().bssid.to_string(),
761                    ssid: ap_state.original().ssid.to_string(),
762                    protection: format!("{:?}", ap_state.original().protection()),
763                    channel: format!("{}", ap_state.original().channel),
764                    ht_cap?: ap_state.original().raw_ht_cap().map(|cap| InspectBytes(cap.bytes)),
765                    vht_cap?: ap_state.original().raw_vht_cap().map(|cap| InspectBytes(cap.bytes)),
766                    wsc?: ap_state.original().probe_resp_wsc().as_ref().map(|wsc| make_inspect_loggable!(
767                            manufacturer: String::from_utf8_lossy(&wsc.manufacturer[..]).to_string(),
768                            model_name: String::from_utf8_lossy(&wsc.model_name[..]).to_string(),
769                            model_number: String::from_utf8_lossy(&wsc.model_number[..]).to_string(),
770                        )),
771                    is_wmm_assoc: ap_state.original().find_wmm_param().is_some(),
772                    wmm_param?: ap_state.original().find_wmm_param().map(InspectBytes),
773                });
774            }
775            Ok(inspector)
776        }
777        .boxed()
778    });
779}
780
781fn inspect_record_external_data(
782    external_inspect_node: &ExternalInspectNode,
783    telemetry_sender: TelemetrySender,
784    defect_sender: mpsc::Sender<Defect>,
785) {
786    external_inspect_node.node.record_lazy_child("connection_status", move || {
787        let telemetry_sender = telemetry_sender.clone();
788        let mut defect_sender = defect_sender.clone();
789        async move {
790            let inspector = Inspector::default();
791            let (sender, receiver) = oneshot::channel();
792            telemetry_sender.send(TelemetryEvent::QueryStatus { sender });
793            let info = match receiver.await {
794                Ok(result) => result.connection_state,
795                Err(e) => {
796                    warn!("Unable to query data for Inspect external node: {}", e);
797                    return Ok(inspector);
798                }
799            };
800
801            if let ConnectionStateInfo::Connected { ap_state, telemetry_proxy, iface_id } = info {
802                inspect_insert!(inspector.root(), connected_network: {
803                    rssi_dbm: ap_state.tracked.signal.rssi_dbm,
804                    snr_db: ap_state.tracked.signal.snr_db,
805                    wsc?: ap_state.original().probe_resp_wsc().as_ref().map(|wsc| make_inspect_loggable!(
806                            manufacturer: String::from_utf8_lossy(&wsc.manufacturer[..]).to_string(),
807                            model_name: String::from_utf8_lossy(&wsc.model_name[..]).to_string(),
808                            model_number: String::from_utf8_lossy(&wsc.model_number[..]).to_string(),
809                        )),
810                });
811
812                if let Some(proxy) = telemetry_proxy {
813                    match proxy.get_histogram_stats()
814                        .on_timeout(GET_IFACE_STATS_TIMEOUT, || {
815                            warn!("Timed out waiting for histogram stats");
816
817                            if let Err(e) = defect_sender
818                                .try_send(Defect::Iface(IfaceFailure::Timeout {
819                                    iface_id,
820                                    source: TimeoutSource::GetHistogramStats,
821                                })) {
822                                    warn!("Failed to report histogram stats defect: {:?}", e)
823                                }
824
825                            Ok(Err(zx::Status::TIMED_OUT.into_raw()))
826                        })
827                        .await {
828                            Ok(Ok(stats)) => {
829                                let mut histograms = HistogramsNode::new(
830                                    inspector.root().create_child("histograms"),
831                                );
832                                if let Some(snr_histograms) = &stats.snr_histograms {
833                                    histograms.log_per_antenna_snr_histograms(&snr_histograms[..]);
834                                }
835                                if let Some(rx_rate_histograms) = &stats.rx_rate_index_histograms {
836                                    histograms.log_per_antenna_rx_rate_histograms(
837                                        &rx_rate_histograms[..],
838                                    );
839                                }
840                                if let Some(noise_floor_histograms) = &stats.noise_floor_histograms {
841                                    histograms.log_per_antenna_noise_floor_histograms(
842                                        &noise_floor_histograms[..],
843                                    );
844                                }
845                                if let Some(rssi_histograms) = &stats.rssi_histograms {
846                                    histograms.log_per_antenna_rssi_histograms(
847                                        &rssi_histograms[..],
848                                    );
849                                }
850
851                                inspector.root().record(histograms);
852                            }
853                            error => {
854                                info!("Error reading histogram stats: {:?}", error);
855                            },
856                        }
857                }
858            }
859            Ok(inspector)
860        }
861        .boxed()
862    });
863}
864
865#[derive(Debug)]
866struct HistogramsNode {
867    node: InspectNode,
868    antenna_nodes: HashMap<fidl_fuchsia_wlan_stats::AntennaId, InspectNode>,
869}
870
871impl InspectType for HistogramsNode {
872    fn into_recorded(self) -> fuchsia_inspect::RecordedInspectType {
873        fuchsia_inspect::RecordedInspectType::Boxed(Box::new(self))
874    }
875}
876
877macro_rules! fn_log_per_antenna_histograms {
878    ($name:ident, $field:ident, $histogram_ty:ty, $sample:ident => $sample_index_expr:expr) => {
879        paste::paste! {
880            pub fn [<log_per_antenna_ $name _histograms>](
881                &mut self,
882                histograms: &[$histogram_ty],
883            ) {
884                for histogram in histograms {
885                    // Only antenna histograms are logged (STATION scope histograms are discarded)
886                    let antenna_id = match &histogram.antenna_id {
887                        Some(id) => **id,
888                        None => continue,
889                    };
890                    let antenna_node = self.create_or_get_antenna_node(antenna_id);
891
892                    let samples = &histogram.$field;
893                    // We expect the driver to send sparse histograms, but filter just in case.
894                    let samples: Vec<_> = samples.iter().filter(|s| s.num_samples > 0).collect();
895                    let array_size = samples.len() * 2;
896                    let histogram_prop_name = concat!(stringify!($name), "_histogram");
897                    let histogram_prop =
898                        antenna_node.create_int_array(histogram_prop_name, array_size);
899
900                    static ONCE: Once = Once::new();
901                    const INSPECT_ARRAY_SIZE_LIMIT: usize = 254;
902                    if array_size > INSPECT_ARRAY_SIZE_LIMIT {
903                        ONCE.call_once(|| {
904                            warn!("{} array size {} > {}. Array may not show up in Inspect",
905                                  histogram_prop_name, array_size, INSPECT_ARRAY_SIZE_LIMIT);
906                        })
907                    }
908
909                    for (i, sample) in samples.iter().enumerate() {
910                        let $sample = sample;
911                        histogram_prop.set(i * 2, $sample_index_expr);
912                        histogram_prop.set(i * 2 + 1, $sample.num_samples as i64);
913                    }
914
915                    let invalid_samples_name = concat!(stringify!($name), "_invalid_samples");
916                    let invalid_samples =
917                        antenna_node.create_uint(invalid_samples_name, histogram.invalid_samples);
918
919                    antenna_node.record(histogram_prop);
920                    antenna_node.record(invalid_samples);
921                }
922            }
923        }
924    };
925}
926
927impl HistogramsNode {
928    pub fn new(node: InspectNode) -> Self {
929        Self { node, antenna_nodes: HashMap::new() }
930    }
931
932    // fn log_per_antenna_snr_histograms
933    fn_log_per_antenna_histograms!(snr, snr_samples, fidl_fuchsia_wlan_stats::SnrHistogram,
934                                   sample => sample.bucket_index as i64);
935    // fn log_per_antenna_rx_rate_histograms
936    fn_log_per_antenna_histograms!(rx_rate, rx_rate_index_samples,
937                                   fidl_fuchsia_wlan_stats::RxRateIndexHistogram,
938                                   sample => sample.bucket_index as i64);
939    // fn log_per_antenna_noise_floor_histograms
940    fn_log_per_antenna_histograms!(noise_floor, noise_floor_samples,
941                                   fidl_fuchsia_wlan_stats::NoiseFloorHistogram,
942                                   sample => sample.bucket_index as i64 - 255);
943    // fn log_per_antenna_rssi_histograms
944    fn_log_per_antenna_histograms!(rssi, rssi_samples, fidl_fuchsia_wlan_stats::RssiHistogram,
945                                   sample => sample.bucket_index as i64 - 255);
946
947    fn create_or_get_antenna_node(
948        &mut self,
949        antenna_id: fidl_fuchsia_wlan_stats::AntennaId,
950    ) -> &mut InspectNode {
951        let histograms_node = &self.node;
952        self.antenna_nodes.entry(antenna_id).or_insert_with(|| {
953            let freq = match antenna_id.freq {
954                fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G => "2Ghz",
955                fidl_fuchsia_wlan_stats::AntennaFreq::Antenna5G => "5Ghz",
956            };
957            let node =
958                histograms_node.create_child(format!("antenna{}_{}", antenna_id.index, freq));
959            node.record_uint("antenna_index", antenna_id.index as u64);
960            node.record_string("antenna_freq", freq);
961            node
962        })
963    }
964}
965
966// Macro wrapper for logging simple events (occurrence, integer, histogram, string)
967// and log a warning when the status is not Ok
968macro_rules! log_cobalt {
969    ($cobalt_proxy:expr, $method_name:ident, $metric_id:expr, $value:expr, $event_codes:expr $(,)?) => {{
970        let status = $cobalt_proxy.$method_name($metric_id, $value, $event_codes).await;
971        match status {
972            Ok(Ok(())) => Ok(()),
973            Ok(Err(e)) => Err(format_err!("Failed logging metric: {}, error: {:?}", $metric_id, e)),
974            Err(e) => Err(format_err!("Failed logging metric: {}, error: {}", $metric_id, e)),
975        }
976    }};
977}
978
979macro_rules! log_cobalt_batch {
980    ($cobalt_proxy:expr, $events:expr, $context:expr $(,)?) => {{
981        if $events.is_empty() {
982            Ok(())
983        } else {
984            let status = $cobalt_proxy.log_metric_events($events).await;
985            match status {
986                Ok(Ok(())) => Ok(()),
987                Ok(Err(e)) => Err(format_err!(
988                    "Failed logging batch metrics, context: {}, error: {:?}",
989                    $context,
990                    e
991                )),
992                Err(e) => Err(format_err!(
993                    "Failed logging batch metrics, context: {}, error: {}",
994                    $context,
995                    e
996                )),
997            }
998        }
999    }};
1000}
1001
1002const INSPECT_SCAN_EVENTS_LIMIT: usize = 7;
1003const INSPECT_CONNECT_EVENTS_LIMIT: usize = 7;
1004const INSPECT_DISCONNECT_EVENTS_LIMIT: usize = 7;
1005const INSPECT_EXTERNAL_DISCONNECT_EVENTS_LIMIT: usize = 2;
1006const INSPECT_ROAM_EVENTS_LIMIT: usize = 7;
1007
1008/// Inspect node with properties queried by external entities.
1009/// Do not change or remove existing properties that are still used.
1010pub struct ExternalInspectNode {
1011    node: InspectNode,
1012    disconnect_events: Mutex<BoundedListNode>,
1013}
1014
1015impl ExternalInspectNode {
1016    pub fn new(node: InspectNode) -> Self {
1017        let disconnect_events = node.create_child("disconnect_events");
1018        Self {
1019            node,
1020            disconnect_events: Mutex::new(BoundedListNode::new(
1021                disconnect_events,
1022                INSPECT_EXTERNAL_DISCONNECT_EVENTS_LIMIT,
1023            )),
1024        }
1025    }
1026}
1027
1028/// Duration without signal before we determine driver as unresponsive
1029const UNRESPONSIVE_FLAG_MIN_DURATION: zx::MonotonicDuration =
1030    zx::MonotonicDuration::from_seconds(60);
1031
1032pub struct Telemetry {
1033    monitor_svc_proxy: fidl_fuchsia_wlan_device_service::DeviceMonitorProxy,
1034    connection_state: ConnectionState,
1035    last_checked_connection_state: fasync::MonotonicInstant,
1036    stats_logger: StatsLogger,
1037
1038    // Inspect properties/nodes that telemetry hangs onto
1039    inspect_node: InspectNode,
1040    get_iface_stats_fail_count: UintProperty,
1041    scan_events_node: Mutex<BoundedListNode>,
1042    connect_events_node: Mutex<BoundedListNode>,
1043    disconnect_events_node: Mutex<BoundedListNode>,
1044    roam_events_node: Mutex<BoundedListNode>,
1045    external_inspect_node: ExternalInspectNode,
1046
1047    // For keeping track of how long client connections were enabled when turning client
1048    // connections on and off.
1049    last_enabled_client_connections: Option<fasync::MonotonicInstant>,
1050
1051    // For keeping track of how long client connections were disabled when turning client
1052    // connections off and on again. None if a command to turn off client connections has never
1053    // been sent or if client connections are on.
1054    last_disabled_client_connections: Option<fasync::MonotonicInstant>,
1055    defect_sender: mpsc::Sender<Defect>,
1056}
1057
1058impl Telemetry {
1059    pub fn new(
1060        telemetry_sender: TelemetrySender,
1061        monitor_svc_proxy: fidl_fuchsia_wlan_device_service::DeviceMonitorProxy,
1062        cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
1063        inspect_node: InspectNode,
1064        external_inspect_node: InspectNode,
1065        defect_sender: mpsc::Sender<Defect>,
1066    ) -> Self {
1067        let stats_logger = StatsLogger::new(cobalt_proxy, &inspect_node);
1068        inspect_record_connection_status(&inspect_node, telemetry_sender.clone());
1069        let get_iface_stats_fail_count = inspect_node.create_uint("get_iface_stats_fail_count", 0);
1070        let scan_events = inspect_node.create_child("scan_events");
1071        let connect_events = inspect_node.create_child("connect_events");
1072        let disconnect_events = inspect_node.create_child("disconnect_events");
1073        let roam_events = inspect_node.create_child("roam_events");
1074        let external_inspect_node = ExternalInspectNode::new(external_inspect_node);
1075        inspect_record_external_data(
1076            &external_inspect_node,
1077            telemetry_sender,
1078            defect_sender.clone(),
1079        );
1080        Self {
1081            monitor_svc_proxy,
1082            connection_state: ConnectionState::Idle(IdleState { connect_start_time: None }),
1083            last_checked_connection_state: fasync::MonotonicInstant::now(),
1084            stats_logger,
1085            inspect_node,
1086            get_iface_stats_fail_count,
1087            scan_events_node: Mutex::new(BoundedListNode::new(
1088                scan_events,
1089                INSPECT_SCAN_EVENTS_LIMIT,
1090            )),
1091            connect_events_node: Mutex::new(BoundedListNode::new(
1092                connect_events,
1093                INSPECT_CONNECT_EVENTS_LIMIT,
1094            )),
1095            disconnect_events_node: Mutex::new(BoundedListNode::new(
1096                disconnect_events,
1097                INSPECT_DISCONNECT_EVENTS_LIMIT,
1098            )),
1099            roam_events_node: Mutex::new(BoundedListNode::new(
1100                roam_events,
1101                INSPECT_ROAM_EVENTS_LIMIT,
1102            )),
1103            external_inspect_node,
1104            last_enabled_client_connections: None,
1105            last_disabled_client_connections: None,
1106            defect_sender,
1107        }
1108    }
1109
1110    pub async fn handle_periodic_telemetry(&mut self) {
1111        let now = fasync::MonotonicInstant::now();
1112        let duration = now - self.last_checked_connection_state;
1113
1114        self.stats_logger.log_stat(StatOp::AddTotalDuration(duration)).await;
1115        self.stats_logger.log_queued_stats().await;
1116
1117        match &mut self.connection_state {
1118            ConnectionState::Idle(..) => (),
1119            ConnectionState::Connected(state) => {
1120                self.stats_logger.log_stat(StatOp::AddConnectedDuration(duration)).await;
1121                if let Some(proxy) = &state.telemetry_proxy {
1122                    match proxy
1123                        .get_iface_stats()
1124                        .on_timeout(GET_IFACE_STATS_TIMEOUT, || {
1125                            warn!("Timed out waiting for iface stats");
1126
1127                            if let Err(e) =
1128                                self.defect_sender.try_send(Defect::Iface(IfaceFailure::Timeout {
1129                                    iface_id: state.iface_id,
1130                                    source: TimeoutSource::GetIfaceStats,
1131                                }))
1132                            {
1133                                warn!("Failed to report iface stats timeout: {:?}", e)
1134                            }
1135
1136                            Ok(Err(zx::Status::TIMED_OUT.into_raw()))
1137                        })
1138                        .await
1139                    {
1140                        Ok(Ok(stats)) => {
1141                            *state.num_consecutive_get_counter_stats_failures.get_mut() = 0;
1142                            if let (Some(prev_connection_stats), Some(current_connection_stats)) = (
1143                                state.prev_connection_stats.as_ref(),
1144                                stats.connection_stats.as_ref(),
1145                            ) {
1146                                diff_and_log_connection_stats(
1147                                    &mut self.stats_logger,
1148                                    prev_connection_stats,
1149                                    current_connection_stats,
1150                                    duration,
1151                                )
1152                                .await;
1153                            }
1154                            state.prev_connection_stats = stats.connection_stats;
1155                        }
1156                        error => {
1157                            info!("Failed to get interface stats: {:?}", error);
1158                            let _ = self.get_iface_stats_fail_count.add(1);
1159                            *state.num_consecutive_get_counter_stats_failures.get_mut() += 1;
1160                            // Safe to unwrap: If we've exceeded 63 bits of consecutive failures,
1161                            // we have other things to worry about.
1162                            #[expect(clippy::unwrap_used)]
1163                            self.stats_logger
1164                                .log_consecutive_counter_stats_failures(
1165                                    (*state.num_consecutive_get_counter_stats_failures)
1166                                        .try_into()
1167                                        .unwrap(),
1168                                )
1169                                .await;
1170                            let _ = state.prev_connection_stats.take();
1171                        }
1172                    }
1173                }
1174
1175                let unresponsive_signal_ind =
1176                    now - state.last_signal_report > UNRESPONSIVE_FLAG_MIN_DURATION;
1177                let mut is_driver_unresponsive = state.is_driver_unresponsive.get_mut();
1178                if unresponsive_signal_ind != *is_driver_unresponsive {
1179                    *is_driver_unresponsive = unresponsive_signal_ind;
1180                    if unresponsive_signal_ind {
1181                        warn!("driver unresponsive due to missing signal report");
1182                    }
1183                }
1184            }
1185            ConnectionState::Disconnected(state) => {
1186                self.stats_logger.log_stat(StatOp::AddDowntimeDuration(duration)).await;
1187                if let Some(prev) = state.latest_no_saved_neighbor_time.take() {
1188                    let duration = now - prev;
1189                    state.accounted_no_saved_neighbor_duration += duration;
1190                    self.stats_logger
1191                        .log_stat(StatOp::AddDowntimeNoSavedNeighborDuration(duration))
1192                        .await;
1193                    state.latest_no_saved_neighbor_time = Some(now);
1194                }
1195            }
1196        }
1197        self.last_checked_connection_state = now;
1198    }
1199
1200    pub async fn handle_telemetry_event(&mut self, event: TelemetryEvent) {
1201        let now = fasync::MonotonicInstant::now();
1202        match event {
1203            TelemetryEvent::QueryStatus { sender } => {
1204                let info = match &self.connection_state {
1205                    ConnectionState::Idle(..) => ConnectionStateInfo::Idle,
1206                    ConnectionState::Disconnected(..) => ConnectionStateInfo::Disconnected,
1207                    ConnectionState::Connected(state) => ConnectionStateInfo::Connected {
1208                        iface_id: state.iface_id,
1209                        ap_state: state.ap_state.clone(),
1210                        telemetry_proxy: state.telemetry_proxy.clone(),
1211                    },
1212                };
1213                let _result = sender.send(QueryStatusResult { connection_state: info });
1214            }
1215            TelemetryEvent::StartEstablishConnection { reset_start_time } => {
1216                match &mut self.connection_state {
1217                    ConnectionState::Idle(IdleState { connect_start_time }) => {
1218                        if reset_start_time || connect_start_time.is_none() {
1219                            let _prev = connect_start_time.replace(now);
1220                        }
1221                    }
1222                    ConnectionState::Disconnected(state) => {
1223                        if reset_start_time || state.connect_start_time.is_none() {
1224                            let _prev = state.connect_start_time.replace(now);
1225                        }
1226                    }
1227                    ConnectionState::Connected(state) => {
1228                        // When in connected state, only set the start time if `reset_start_time` is
1229                        // true because it indicates the user triggers the new connect action.
1230                        if reset_start_time {
1231                            let _prev = state.new_connect_start_time.replace(now);
1232                        }
1233                    }
1234                }
1235            }
1236            TelemetryEvent::ClearEstablishConnectionStartTime => match &mut self.connection_state {
1237                ConnectionState::Idle(state) => {
1238                    let _start_time = state.connect_start_time.take();
1239                }
1240                ConnectionState::Disconnected(state) => {
1241                    let _start_time = state.connect_start_time.take();
1242                }
1243                ConnectionState::Connected(state) => {
1244                    let _start_time = state.new_connect_start_time.take();
1245                }
1246            },
1247            TelemetryEvent::ActiveScanRequested { num_ssids_requested } => {
1248                self.stats_logger
1249                    .log_active_scan_requested_cobalt_metrics(num_ssids_requested)
1250                    .await
1251            }
1252            TelemetryEvent::ActiveScanRequestedViaApi { num_ssids_requested } => {
1253                self.stats_logger
1254                    .log_active_scan_requested_via_api_cobalt_metrics(num_ssids_requested)
1255                    .await
1256            }
1257            TelemetryEvent::NetworkSelectionDecision {
1258                network_selection_type,
1259                num_candidates,
1260                selected_count,
1261            } => {
1262                self.stats_logger
1263                    .log_network_selection_metrics(
1264                        &mut self.connection_state,
1265                        network_selection_type,
1266                        num_candidates,
1267                        selected_count,
1268                    )
1269                    .await;
1270            }
1271            TelemetryEvent::ConnectResult {
1272                iface_id,
1273                policy_connect_reason,
1274                result,
1275                multiple_bss_candidates,
1276                ap_state,
1277                network_is_likely_hidden,
1278            } => {
1279                let connect_start_time = match &self.connection_state {
1280                    ConnectionState::Idle(state) => state.connect_start_time,
1281                    ConnectionState::Disconnected(state) => state.connect_start_time,
1282                    ConnectionState::Connected(..) => {
1283                        warn!("Received ConnectResult event while still connected");
1284                        None
1285                    }
1286                };
1287                self.stats_logger
1288                    .report_connect_result(
1289                        policy_connect_reason,
1290                        result.code,
1291                        multiple_bss_candidates,
1292                        &ap_state,
1293                        connect_start_time,
1294                    )
1295                    .await;
1296                self.stats_logger.log_stat(StatOp::AddConnectAttemptsCount).await;
1297                if result.code == fidl_ieee80211::StatusCode::Success {
1298                    self.log_connect_event_inspect(&ap_state, multiple_bss_candidates);
1299                    self.stats_logger.log_stat(StatOp::AddConnectSuccessfulCount).await;
1300
1301                    self.stats_logger
1302                        .log_device_connected_cobalt_metrics(
1303                            multiple_bss_candidates,
1304                            &ap_state,
1305                            network_is_likely_hidden,
1306                        )
1307                        .await;
1308                    if let ConnectionState::Disconnected(state) = &self.connection_state {
1309                        if state.latest_no_saved_neighbor_time.is_some() {
1310                            warn!("'No saved neighbor' flag still set even though connected");
1311                        }
1312                        self.stats_logger.queue_stat_op(StatOp::AddDowntimeDuration(
1313                            now - self.last_checked_connection_state,
1314                        ));
1315                        let total_downtime = now - state.disconnected_since;
1316                        if total_downtime < state.accounted_no_saved_neighbor_duration {
1317                            warn!(
1318                                "Total downtime is less than no-saved-neighbor duration. \
1319                                 Total downtime: {:?}, No saved neighbor duration: {:?}",
1320                                total_downtime, state.accounted_no_saved_neighbor_duration
1321                            )
1322                        }
1323                        let adjusted_downtime = max(
1324                            total_downtime - state.accounted_no_saved_neighbor_duration,
1325                            zx::MonotonicDuration::from_seconds(0),
1326                        );
1327
1328                        if let Some(disconnect_info) = state.disconnect_info.as_ref() {
1329                            self.stats_logger
1330                                .log_downtime_cobalt_metrics(adjusted_downtime, disconnect_info)
1331                                .await;
1332                            self.stats_logger
1333                                .log_reconnect_cobalt_metrics(
1334                                    total_downtime,
1335                                    disconnect_info.disconnect_source,
1336                                )
1337                                .await;
1338                        }
1339                    }
1340
1341                    // Log successful post-recovery connection attempt if relevant.
1342                    if let Some(recovery_reason) =
1343                        self.stats_logger.recovery_record.connect_failure.take()
1344                    {
1345                        self.stats_logger
1346                            .log_post_recovery_result(recovery_reason, RecoveryOutcome::Success)
1347                            .await
1348                    }
1349
1350                    let (proxy, server) = fidl::endpoints::create_proxy();
1351                    let telemetry_proxy = match self
1352                        .monitor_svc_proxy
1353                        .get_sme_telemetry(iface_id, server)
1354                        .await
1355                    {
1356                        Ok(Ok(())) => Some(proxy),
1357                        Ok(Err(e)) => {
1358                            error!(
1359                                "Request for SME telemetry for iface {} completed with error {}. No telemetry will be captured.",
1360                                iface_id, e
1361                            );
1362                            None
1363                        }
1364                        Err(e) => {
1365                            error!(
1366                                "Failed to request SME telemetry for iface {} with error {}. No telemetry will be captured.",
1367                                iface_id, e
1368                            );
1369                            None
1370                        }
1371                    };
1372                    self.connection_state = ConnectionState::Connected(Box::new(ConnectedState {
1373                        iface_id,
1374                        new_connect_start_time: None,
1375                        prev_connection_stats: None,
1376                        multiple_bss_candidates,
1377                        ap_state: Box::new(ap_state),
1378                        network_is_likely_hidden,
1379
1380                        // We have not received a signal report yet, but since this is used as
1381                        // indicator for whether driver is still responsive, set it to the
1382                        // connection start time for now.
1383                        last_signal_report: now,
1384                        // TODO(https://fxbug.dev/404889275): Consider renaming the Inspect
1385                        // property name to no longer to refer to "counter"
1386                        num_consecutive_get_counter_stats_failures: InspectableU64::new(
1387                            0,
1388                            &self.inspect_node,
1389                            "num_consecutive_get_counter_stats_failures",
1390                        ),
1391                        is_driver_unresponsive: InspectableBool::new(
1392                            false,
1393                            &self.inspect_node,
1394                            "is_driver_unresponsive",
1395                        ),
1396
1397                        telemetry_proxy,
1398                    }));
1399                    self.last_checked_connection_state = now;
1400                } else if !result.is_credential_rejected {
1401                    // In the case where the connection failed for a reason other than a credential
1402                    // mismatch, log a connection failure occurrence metric.
1403                    self.stats_logger.log_connection_failure().await;
1404
1405                    // Log failed post-recovery connection attempt if relevant.
1406                    if let Some(recovery_reason) =
1407                        self.stats_logger.recovery_record.connect_failure.take()
1408                    {
1409                        self.stats_logger
1410                            .log_post_recovery_result(recovery_reason, RecoveryOutcome::Failure)
1411                            .await
1412                    }
1413                }
1414
1415                // Any completed SME operation tells us the SME is operational.
1416                self.report_sme_timeout_resolved().await;
1417            }
1418            TelemetryEvent::PolicyInitiatedRoamResult {
1419                iface_id,
1420                result,
1421                updated_ap_state,
1422                original_ap_state,
1423                request,
1424                request_time,
1425                result_time,
1426            } => {
1427                if result.status_code == fidl_ieee80211::StatusCode::Success {
1428                    match &self.connection_state {
1429                        ConnectionState::Connected(state) => {
1430                            // Update telemetry module internal state to reflect the start of a new
1431                            // BSS connection.
1432                            self.connection_state =
1433                                ConnectionState::Connected(Box::new(ConnectedState {
1434                                    iface_id,
1435                                    new_connect_start_time: None,
1436                                    prev_connection_stats: None,
1437                                    multiple_bss_candidates: state.multiple_bss_candidates,
1438                                    ap_state: Box::new(updated_ap_state.clone()),
1439                                    network_is_likely_hidden: state.network_is_likely_hidden,
1440
1441                                    // We have not received a signal report yet, but since this is used as
1442                                    // indicator for whether driver is still responsive, set it to the
1443                                    // connection start time for now.
1444                                    last_signal_report: now,
1445                                    // TODO(https://fxbug.dev/404889275): Consider renaming the Inspect
1446                                    // property name to no longer to refer to "counter"
1447                                    num_consecutive_get_counter_stats_failures: InspectableU64::new(
1448                                        0,
1449                                        &self.inspect_node,
1450                                        "num_consecutive_get_counter_stats_failures",
1451                                    ),
1452                                    is_driver_unresponsive: InspectableBool::new(
1453                                        false,
1454                                        &self.inspect_node,
1455                                        "is_driver_unresponsive",
1456                                    ),
1457
1458                                    telemetry_proxy: state.telemetry_proxy.clone(),
1459                                }));
1460                            self.last_checked_connection_state = now;
1461                            // TODO(https://fxbug.dev/135975) Log roam success to Cobalt and Inspect.
1462                        }
1463                        _ => {
1464                            warn!(
1465                                "Unexpectedly received a successful roam event while telemetry module ConnectionState is not Connected."
1466                            );
1467                        }
1468                    }
1469                }
1470                // Log roam event to Inspect
1471                self.log_roam_event_inspect(iface_id, &result, &request);
1472
1473                // Log metrics following a roam result
1474                self.stats_logger
1475                    .log_roam_result_metrics(
1476                        result,
1477                        updated_ap_state,
1478                        original_ap_state,
1479                        request,
1480                        request_time,
1481                        result_time,
1482                    )
1483                    .await;
1484            }
1485            TelemetryEvent::Disconnected { track_subsequent_downtime, info } => {
1486                let mut connect_start_time = None;
1487
1488                // Disconnect info is expected to be None when something unexpectedly fails beneath
1489                // the SME. This case is very rare, so we're ok with missing metrics in this case.
1490                if let Some(info) = info.as_ref() {
1491                    // Any completed SME operation tells us the SME is operational.
1492                    // A caveat here is that empty disconnect info indicates that something beneath
1493                    // SME has failed.
1494                    self.report_sme_timeout_resolved().await;
1495
1496                    self.log_disconnect_event_inspect(info);
1497                    self.stats_logger
1498                        .log_stat(StatOp::AddDisconnectCount(info.disconnect_source))
1499                        .await;
1500                    self.stats_logger
1501                        .log_pre_disconnect_score_deltas_by_signal(
1502                            info.connected_duration,
1503                            info.signals.clone(),
1504                        )
1505                        .await;
1506                    self.stats_logger
1507                        .log_pre_disconnect_rssi_deltas(
1508                            info.connected_duration,
1509                            info.signals.clone(),
1510                        )
1511                        .await;
1512
1513                    // If we are in the connected state, log the disconnect and short connection
1514                    // metric if applicable.
1515                    if let ConnectionState::Connected(state) = &self.connection_state {
1516                        self.stats_logger
1517                            .log_disconnect_cobalt_metrics(info, state.multiple_bss_candidates)
1518                            .await;
1519
1520                        // Log metrics if connection had a short duration.
1521                        if info.connected_duration < METRICS_SHORT_CONNECT_DURATION {
1522                            self.stats_logger
1523                                .log_short_duration_connection_metrics(
1524                                    info.signals.clone(),
1525                                    info.disconnect_source,
1526                                    info.previous_connect_reason,
1527                                )
1528                                .await;
1529                        }
1530                    }
1531
1532                    // If `is_sme_reconnecting` is true, we already know that the process of
1533                    // establishing connection is already started at the moment of disconnect,
1534                    // so set the connect_start_time to now.
1535                    if info.is_sme_reconnecting {
1536                        connect_start_time = Some(now);
1537                    } else if let ConnectionState::Connected(state) = &self.connection_state {
1538                        connect_start_time = state.new_connect_start_time
1539                    }
1540                }
1541
1542                let duration = now - self.last_checked_connection_state;
1543                match &self.connection_state {
1544                    ConnectionState::Connected(state) => {
1545                        self.stats_logger.queue_stat_op(StatOp::AddConnectedDuration(duration));
1546                        // Log device connected to AP metrics right now in case we have not logged it
1547                        // to Cobalt yet today.
1548                        self.stats_logger
1549                            .log_device_connected_cobalt_metrics(
1550                                state.multiple_bss_candidates,
1551                                &state.ap_state,
1552                                state.network_is_likely_hidden,
1553                            )
1554                            .await;
1555                    }
1556                    _ => {
1557                        warn!(
1558                            "Received disconnect event while not connected. Metric may not be logged"
1559                        );
1560                    }
1561                }
1562
1563                self.connection_state = if track_subsequent_downtime {
1564                    ConnectionState::Disconnected(Box::new(DisconnectedState {
1565                        disconnected_since: now,
1566                        disconnect_info: info,
1567                        connect_start_time,
1568                        // We assume that there's a saved neighbor in vicinity until proven
1569                        // otherwise from scan result.
1570                        latest_no_saved_neighbor_time: None,
1571                        accounted_no_saved_neighbor_duration: zx::MonotonicDuration::from_seconds(
1572                            0,
1573                        ),
1574                    }))
1575                } else {
1576                    ConnectionState::Idle(IdleState { connect_start_time })
1577                };
1578                self.last_checked_connection_state = now;
1579            }
1580            TelemetryEvent::OnSignalReport { ind } => {
1581                if let ConnectionState::Connected(state) = &mut self.connection_state {
1582                    if let Some(rssi) = ind.rssi_dbm {
1583                        state.ap_state.tracked.signal.rssi_dbm = rssi;
1584                        self.stats_logger
1585                            .log_signal_report_metrics(rssi, ind.tx_rate_500kbps)
1586                            .await;
1587                    }
1588                    if let Some(snr) = ind.snr_db {
1589                        state.ap_state.tracked.signal.snr_db = snr;
1590                    }
1591                    if ind.rssi_dbm.is_some() || ind.snr_db.is_some() {
1592                        state.last_signal_report = now;
1593                    }
1594                }
1595            }
1596            TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity } => {
1597                self.stats_logger.log_signal_velocity_metrics(rssi_velocity).await;
1598            }
1599            TelemetryEvent::OnChannelSwitched { info } => {
1600                if let ConnectionState::Connected(state) = &mut self.connection_state {
1601                    let cbw = match Bandwidth::from_fidl(
1602                        info.bandwidth,
1603                        info.vht_secondary_80_channel.number,
1604                    ) {
1605                        Ok(cbw) => cbw,
1606                        Err(e) => {
1607                            // In the event that the CBW is invalid, reuse the previous CBW
1608                            // in determining the client's channel to preserve any legacy
1609                            // behavior.
1610                            error!("Invalid CBW in ChannelSwitchInfo: {}", e);
1611                            state.ap_state.tracked.channel.bandwidth
1612                        }
1613                    };
1614                    state.ap_state.tracked.channel = Channel::new(
1615                        info.new_primary_channel.number,
1616                        cbw,
1617                        info.new_primary_channel.band,
1618                    );
1619                }
1620            }
1621            TelemetryEvent::PolicyRoamScan { reasons } => {
1622                self.stats_logger.log_policy_roam_scan_metrics(reasons).await;
1623            }
1624            TelemetryEvent::PolicyRoamAttempt { request, connected_duration } => {
1625                self.stats_logger
1626                    .log_policy_roam_attempt_metrics(request, connected_duration)
1627                    .await;
1628            }
1629            TelemetryEvent::WouldRoamConnect => {
1630                self.stats_logger.log_would_roam_connect().await;
1631            }
1632            TelemetryEvent::SavedNetworkCount {
1633                saved_network_count,
1634                config_count_per_saved_network,
1635            } => {
1636                self.stats_logger
1637                    .log_saved_network_counts(saved_network_count, config_count_per_saved_network)
1638                    .await;
1639            }
1640            TelemetryEvent::NetworkSelectionScanInterval { time_since_last_scan } => {
1641                self.stats_logger.log_network_selection_scan_interval(time_since_last_scan).await;
1642            }
1643            TelemetryEvent::ConnectionSelectionScanResults {
1644                saved_network_count,
1645                bss_count_per_saved_network,
1646                saved_network_count_found_by_active_scan,
1647            } => {
1648                self.stats_logger
1649                    .log_connection_selection_scan_results(
1650                        saved_network_count,
1651                        bss_count_per_saved_network,
1652                        saved_network_count_found_by_active_scan,
1653                    )
1654                    .await;
1655            }
1656            TelemetryEvent::StartClientConnectionsRequest => {
1657                let now = fasync::MonotonicInstant::now();
1658                if self.last_enabled_client_connections.is_none() {
1659                    self.last_enabled_client_connections = Some(now);
1660                }
1661                self.last_disabled_client_connections = None;
1662            }
1663            TelemetryEvent::StopClientConnectionsRequest => {
1664                let now = fasync::MonotonicInstant::now();
1665                // Do not change the time if the request to turn off connections comes in when
1666                // client connections are already stopped.
1667                if self.last_disabled_client_connections.is_none() {
1668                    self.last_disabled_client_connections = Some(fasync::MonotonicInstant::now());
1669                }
1670                if let Some(enabled_time) = self.last_enabled_client_connections {
1671                    let enabled_duration = now - enabled_time;
1672                    self.stats_logger.log_stop_client_connections_request(enabled_duration).await
1673                }
1674                self.last_enabled_client_connections = None;
1675            }
1676            TelemetryEvent::StopAp { enabled_duration } => {
1677                self.stats_logger.log_stop_ap_cobalt_metrics(enabled_duration).await;
1678
1679                // Any completed SME operation tells us the SME is operational.
1680                self.report_sme_timeout_resolved().await;
1681            }
1682            TelemetryEvent::IfaceCreationResult { result, .. } => {
1683                self.stats_logger.log_iface_creation_result(result.map(|_| ())).await;
1684            }
1685            TelemetryEvent::IfaceDestructionResult { result, .. } => {
1686                self.stats_logger.log_iface_destruction_result(result.map(|_| ())).await;
1687            }
1688            TelemetryEvent::StartApResult(result) => {
1689                self.stats_logger.log_ap_start_result(result).await;
1690
1691                // Any completed SME operation tells us the SME is operational.
1692                self.report_sme_timeout_resolved().await;
1693            }
1694            TelemetryEvent::ScanRequestFulfillmentTime { duration, reason } => {
1695                self.stats_logger.log_scan_request_fulfillment_time(duration, reason).await;
1696            }
1697            TelemetryEvent::ScanQueueStatistics { fulfilled_requests, remaining_requests } => {
1698                self.stats_logger
1699                    .log_scan_queue_statistics(fulfilled_requests, remaining_requests)
1700                    .await;
1701            }
1702            TelemetryEvent::BssSelectionResult {
1703                reason,
1704                scored_candidates,
1705                selected_candidate,
1706            } => {
1707                self.stats_logger
1708                    .log_bss_selection_metrics(reason, scored_candidates, selected_candidate)
1709                    .await
1710            }
1711            TelemetryEvent::PostConnectionSignals { connect_time, signal_at_connect, signals } => {
1712                self.stats_logger
1713                    .log_post_connection_score_deltas_by_signal(
1714                        connect_time,
1715                        signal_at_connect,
1716                        signals.clone(),
1717                    )
1718                    .await;
1719                self.stats_logger
1720                    .log_post_connection_rssi_deltas(connect_time, signal_at_connect, signals)
1721                    .await;
1722            }
1723            TelemetryEvent::ScanEvent { inspect_data, scan_defects } => {
1724                self.log_scan_event_inspect(inspect_data);
1725                self.stats_logger.log_scan_issues(scan_defects).await;
1726
1727                // Any completed SME operation tells us the SME is operational.
1728                self.report_sme_timeout_resolved().await;
1729            }
1730            TelemetryEvent::LongDurationSignals { signals } => {
1731                self.stats_logger
1732                    .log_connection_score_average_by_signal(
1733                        metrics::ConnectionScoreAverageMetricDimensionDuration::LongDuration as u32,
1734                        signals.clone(),
1735                    )
1736                    .await;
1737                self.stats_logger
1738                    .log_connection_rssi_average(
1739                        metrics::ConnectionRssiAverageMetricDimensionDuration::LongDuration as u32,
1740                        signals,
1741                    )
1742                    .await;
1743            }
1744            TelemetryEvent::RecoveryEvent { reason } => {
1745                self.stats_logger.log_recovery_occurrence(reason).await;
1746            }
1747            TelemetryEvent::SmeTimeout { .. } => {
1748                // If timeouts have been a consistent issue to the point that recovery has been
1749                // requested and operations are still timing out, record a recovery failure.
1750                if let Some(recovery_reason) = self.stats_logger.recovery_record.timeout.take() {
1751                    self.stats_logger
1752                        .log_post_recovery_result(recovery_reason, RecoveryOutcome::Failure)
1753                        .await
1754                }
1755            }
1756            TelemetryEvent::SmeScanStart | TelemetryEvent::SmeScanResult { .. } => {}
1757            TelemetryEvent::ChipPowerUpFailure => {
1758                self.stats_logger.log_chip_power_up_failure().await;
1759            }
1760        }
1761    }
1762
1763    pub fn log_scan_event_inspect(&self, scan_event_info: ScanEventInspectData) {
1764        if !scan_event_info.unknown_protection_ies.is_empty() {
1765            inspect_log!(self.scan_events_node.lock(), {
1766                unknown_protection_ies: InspectList(&scan_event_info.unknown_protection_ies)
1767            });
1768        }
1769    }
1770
1771    pub fn log_connect_event_inspect(
1772        &self,
1773        ap_state: &client::types::ApState,
1774        multiple_bss_candidates: bool,
1775    ) {
1776        inspect_log!(self.connect_events_node.lock(), {
1777            multiple_bss_candidates: multiple_bss_candidates,
1778            network: {
1779                bssid: ap_state.original().bssid.to_string(),
1780                ssid: ap_state.original().ssid.to_string(),
1781                rssi_dbm: ap_state.tracked.signal.rssi_dbm,
1782                snr_db: ap_state.tracked.signal.snr_db,
1783            },
1784        });
1785    }
1786
1787    pub fn log_disconnect_event_inspect(&self, info: &DisconnectInfo) {
1788        inspect_log!(self.disconnect_events_node.lock(), {
1789            connected_duration: info.connected_duration.into_nanos(),
1790            disconnect_source: info.disconnect_source.inspect_string(),
1791            network: {
1792                rssi_dbm: info.ap_state.tracked.signal.rssi_dbm,
1793                snr_db: info.ap_state.tracked.signal.snr_db,
1794                bssid: info.ap_state.original().bssid.to_string(),
1795                ssid: info.ap_state.original().ssid.to_string(),
1796                protection: format!("{:?}", info.ap_state.original().protection()),
1797                channel: format!("{}", info.ap_state.tracked.channel),
1798                ht_cap?: info.ap_state.original().raw_ht_cap().map(|cap| InspectBytes(cap.bytes)),
1799                vht_cap?: info.ap_state.original().raw_vht_cap().map(|cap| InspectBytes(cap.bytes)),
1800                wsc?: info.ap_state.original().probe_resp_wsc().as_ref().map(|wsc| make_inspect_loggable!(
1801                        manufacturer: String::from_utf8_lossy(&wsc.manufacturer[..]).to_string(),
1802                        model_name: String::from_utf8_lossy(&wsc.model_name[..]).to_string(),
1803                        model_number: String::from_utf8_lossy(&wsc.model_number[..]).to_string(),
1804                    )),
1805                is_wmm_assoc: info.ap_state.original().find_wmm_param().is_some(),
1806                wmm_param?: info.ap_state.original().find_wmm_param().map(InspectBytes),
1807            }
1808        });
1809        inspect_log!(self.external_inspect_node.disconnect_events.lock(), {
1810            // Flatten the reason code for external consumer as their reason code metric
1811            // cannot easily be adjusted to accept an additional dimension.
1812            flattened_reason_code: info.disconnect_source.flattened_reason_code(),
1813            locally_initiated: info.disconnect_source.locally_initiated(),
1814            network: {
1815                channel: {
1816                    primary: info.ap_state.tracked.channel.primary,
1817                },
1818            },
1819        });
1820    }
1821
1822    pub fn log_roam_event_inspect(
1823        &self,
1824        iface_id: u16,
1825        result: &fidl_sme::RoamResult,
1826        request: &PolicyRoamRequest,
1827    ) {
1828        inspect_log!(self.roam_events_node.lock(), {
1829            iface_id: iface_id,
1830            target: {
1831                ssid: request.candidate.network.ssid.to_string(),
1832                bssid: request.candidate.bss.bssid.to_string(),
1833            },
1834            reasons: InspectList(request.reasons.iter().map(|reason| format!("{reason:?}")).collect::<Vec<String>>().as_slice()),
1835            status: result.status_code.into_primitive(),
1836            original_association_maintained: result.original_association_maintained,
1837        });
1838    }
1839
1840    pub async fn log_daily_cobalt_metrics(&mut self) {
1841        self.stats_logger.log_daily_cobalt_metrics().await;
1842        if let ConnectionState::Connected(state) = &self.connection_state {
1843            self.stats_logger
1844                .log_device_connected_cobalt_metrics(
1845                    state.multiple_bss_candidates,
1846                    &state.ap_state,
1847                    state.network_is_likely_hidden,
1848                )
1849                .await;
1850        }
1851    }
1852
1853    pub async fn signal_hr_passed(&mut self) {
1854        self.stats_logger.handle_hr_passed().await;
1855    }
1856
1857    // Any return from an SME request is considered a successful outcome of a recovery intervention.
1858    pub async fn report_sme_timeout_resolved(&mut self) {
1859        if let Some(recovery_reason) = self.stats_logger.recovery_record.timeout.take() {
1860            self.stats_logger
1861                .log_post_recovery_result(recovery_reason, RecoveryOutcome::Success)
1862                .await
1863        }
1864    }
1865}
1866
1867// Convert float to an integer in "ten thousandth" unit
1868// Example: 0.02f64 (i.e. 2%) -> 200 per ten thousand
1869fn float_to_ten_thousandth(value: f64) -> i64 {
1870    (value * 10000f64) as i64
1871}
1872
1873pub async fn connect_to_metrics_logger_factory()
1874-> Result<fidl_fuchsia_metrics::MetricEventLoggerFactoryProxy, Error> {
1875    let cobalt_svc = fuchsia_component::client::connect_to_protocol::<
1876        fidl_fuchsia_metrics::MetricEventLoggerFactoryMarker,
1877    >()
1878    .context("failed to connect to metrics service")?;
1879    Ok(cobalt_svc)
1880}
1881
1882// Communicates with the MetricEventLoggerFactory service to create a MetricEventLoggerProxy for
1883// the caller.
1884pub async fn create_metrics_logger(
1885    factory_proxy: &fidl_fuchsia_metrics::MetricEventLoggerFactoryProxy,
1886) -> Result<fidl_fuchsia_metrics::MetricEventLoggerProxy, Error> {
1887    let (cobalt_proxy, cobalt_server) =
1888        fidl::endpoints::create_proxy::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
1889
1890    let project_spec = fidl_fuchsia_metrics::ProjectSpec {
1891        customer_id: None, // defaults to fuchsia
1892        project_id: Some(metrics::PROJECT_ID),
1893        ..Default::default()
1894    };
1895
1896    let status = factory_proxy
1897        .create_metric_event_logger(&project_spec, cobalt_server)
1898        .await
1899        .context("failed to create metrics event logger")?;
1900
1901    match status {
1902        Ok(_) => Ok(cobalt_proxy),
1903        Err(err) => Err(format_err!("failed to create metrics event logger: {:?}", err)),
1904    }
1905}
1906
1907const HIGH_PACKET_DROP_RATE_THRESHOLD: f64 = 0.02;
1908const VERY_HIGH_PACKET_DROP_RATE_THRESHOLD: f64 = 0.05;
1909
1910const DEVICE_LOW_CONNECTION_SUCCESS_RATE_THRESHOLD: f64 = 0.1;
1911
1912async fn diff_and_log_connection_stats(
1913    stats_logger: &mut StatsLogger,
1914    prev: &fidl_fuchsia_wlan_stats::ConnectionStats,
1915    current: &fidl_fuchsia_wlan_stats::ConnectionStats,
1916    duration: zx::MonotonicDuration,
1917) {
1918    // Early return if the counters have dropped. This indicates that the counters have reset
1919    // due to reasons like PHY reset. Counters being reset due to re-connection is already
1920    // handled outside this function.
1921    match (current.rx_unicast_total, prev.rx_unicast_total) {
1922        (Some(current), Some(prev)) if current < prev => return,
1923        _ => (),
1924    }
1925    match (current.rx_unicast_drop, prev.rx_unicast_drop) {
1926        (Some(current), Some(prev)) if current < prev => return,
1927        _ => (),
1928    }
1929    match (current.tx_total, prev.tx_total) {
1930        (Some(current), Some(prev)) if current < prev => return,
1931        _ => (),
1932    }
1933    match (current.tx_drop, prev.tx_drop) {
1934        (Some(current), Some(prev)) if current < prev => return,
1935        _ => (),
1936    }
1937
1938    diff_and_log_rx_counters(stats_logger, prev, current, duration).await;
1939    diff_and_log_tx_counters(stats_logger, prev, current, duration).await;
1940}
1941
1942async fn diff_and_log_rx_counters(
1943    stats_logger: &mut StatsLogger,
1944    prev: &fidl_fuchsia_wlan_stats::ConnectionStats,
1945    current: &fidl_fuchsia_wlan_stats::ConnectionStats,
1946    duration: zx::MonotonicDuration,
1947) {
1948    let (current_rx_unicast_total, prev_rx_unicast_total) =
1949        match (current.rx_unicast_total, prev.rx_unicast_total) {
1950            (Some(current), Some(prev)) => (current, prev),
1951            _ => return,
1952        };
1953    let (current_rx_unicast_drop, prev_rx_unicast_drop) =
1954        match (current.rx_unicast_drop, prev.rx_unicast_drop) {
1955            (Some(current), Some(prev)) => (current, prev),
1956            _ => return,
1957        };
1958
1959    let rx_total: u64 = match current_rx_unicast_total.checked_sub(prev_rx_unicast_total) {
1960        Some(diff) => diff,
1961        _ => return,
1962    };
1963    let rx_drop = match current_rx_unicast_drop.checked_sub(prev_rx_unicast_drop) {
1964        Some(diff) => diff,
1965        _ => return,
1966    };
1967    let rx_drop_rate = if rx_total > 0 { rx_drop as f64 / rx_total as f64 } else { 0f64 };
1968
1969    if rx_drop_rate > HIGH_PACKET_DROP_RATE_THRESHOLD {
1970        stats_logger.log_stat(StatOp::AddRxHighPacketDropDuration(duration)).await;
1971    }
1972    if rx_drop_rate > VERY_HIGH_PACKET_DROP_RATE_THRESHOLD {
1973        stats_logger.log_stat(StatOp::AddRxVeryHighPacketDropDuration(duration)).await;
1974    }
1975    if rx_total == 0 {
1976        stats_logger.log_stat(StatOp::AddNoRxDuration(duration)).await;
1977    }
1978}
1979
1980async fn diff_and_log_tx_counters(
1981    stats_logger: &mut StatsLogger,
1982    prev: &fidl_fuchsia_wlan_stats::ConnectionStats,
1983    current: &fidl_fuchsia_wlan_stats::ConnectionStats,
1984    duration: zx::MonotonicDuration,
1985) {
1986    let (current_tx_total, prev_tx_total) = match (current.tx_total, prev.tx_total) {
1987        (Some(current), Some(prev)) => (current, prev),
1988        _ => return,
1989    };
1990    let (current_tx_drop, prev_tx_drop) = match (current.tx_drop, prev.tx_drop) {
1991        (Some(current), Some(prev)) => (current, prev),
1992        _ => return,
1993    };
1994
1995    let tx_total = match current_tx_total.checked_sub(prev_tx_total) {
1996        Some(diff) => diff,
1997        _ => return,
1998    };
1999    let tx_drop = match current_tx_drop.checked_sub(prev_tx_drop) {
2000        Some(diff) => diff,
2001        _ => return,
2002    };
2003    let tx_drop_rate = if tx_total > 0 { tx_drop as f64 / tx_total as f64 } else { 0f64 };
2004
2005    if tx_drop_rate > HIGH_PACKET_DROP_RATE_THRESHOLD {
2006        stats_logger.log_stat(StatOp::AddTxHighPacketDropDuration(duration)).await;
2007    }
2008    if tx_drop_rate > VERY_HIGH_PACKET_DROP_RATE_THRESHOLD {
2009        stats_logger.log_stat(StatOp::AddTxVeryHighPacketDropDuration(duration)).await;
2010    }
2011}
2012
2013struct StatsLogger {
2014    cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
2015    last_1d_stats: Arc<Mutex<WindowedStats<StatCounters>>>,
2016    last_7d_stats: Arc<Mutex<WindowedStats<StatCounters>>>,
2017    last_successful_recovery: UintProperty,
2018    successful_recoveries: UintProperty,
2019    /// Stats aggregated for each day and then logged into Cobalt.
2020    /// As these stats are more detailed than `last_1d_stats`, we do not track per-hour
2021    /// windowed stats in order to reduce space and heap allocation. Instead, these stats
2022    /// are logged to Cobalt once every 24 hours and then cleared. Additionally, these
2023    /// are not logged into Inspect.
2024    last_1d_detailed_stats: DailyDetailedStats,
2025    stat_ops: Vec<StatOp>,
2026    hr_tick: u32,
2027    rssi_velocity_hist: HashMap<u32, fidl_fuchsia_metrics::HistogramBucket>,
2028    rssi_hist: HashMap<u32, fidl_fuchsia_metrics::HistogramBucket>,
2029    recovery_record: RecoveryRecord,
2030    throttled_error_logger: ThrottledErrorLogger,
2031
2032    // Inspect nodes
2033    _1d_counters_inspect_node: LazyNode,
2034    _7d_counters_inspect_node: LazyNode,
2035}
2036
2037impl StatsLogger {
2038    pub fn new(
2039        cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
2040        inspect_node: &InspectNode,
2041    ) -> Self {
2042        let last_1d_stats = Arc::new(Mutex::new(WindowedStats::new(24)));
2043        let last_7d_stats = Arc::new(Mutex::new(WindowedStats::new(7)));
2044        let last_successful_recovery = inspect_node.create_uint("last_successful_recovery", 0);
2045        let successful_recoveries = inspect_node.create_uint("successful_recoveries", 0);
2046        let _1d_counters_inspect_node =
2047            inspect_create_counters(inspect_node, "1d_counters", Arc::clone(&last_1d_stats));
2048        let _7d_counters_inspect_node =
2049            inspect_create_counters(inspect_node, "7d_counters", Arc::clone(&last_7d_stats));
2050
2051        Self {
2052            cobalt_proxy,
2053            last_1d_stats,
2054            last_7d_stats,
2055            last_successful_recovery,
2056            successful_recoveries,
2057            last_1d_detailed_stats: DailyDetailedStats::new(),
2058            stat_ops: vec![],
2059            hr_tick: 0,
2060            rssi_velocity_hist: HashMap::new(),
2061            rssi_hist: HashMap::new(),
2062            recovery_record: RecoveryRecord::new(),
2063            throttled_error_logger: ThrottledErrorLogger::new(
2064                MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS,
2065            ),
2066            _1d_counters_inspect_node,
2067            _7d_counters_inspect_node,
2068        }
2069    }
2070
2071    async fn log_stat(&mut self, stat_op: StatOp) {
2072        self.log_stat_counters(stat_op);
2073    }
2074
2075    fn log_stat_counters(&mut self, stat_op: StatOp) {
2076        let zero = StatCounters::default();
2077        let addition = match stat_op {
2078            StatOp::AddTotalDuration(duration) => StatCounters { total_duration: duration, ..zero },
2079            StatOp::AddConnectedDuration(duration) => {
2080                StatCounters { connected_duration: duration, ..zero }
2081            }
2082            StatOp::AddDowntimeDuration(duration) => {
2083                StatCounters { downtime_duration: duration, ..zero }
2084            }
2085            StatOp::AddDowntimeNoSavedNeighborDuration(duration) => {
2086                StatCounters { downtime_no_saved_neighbor_duration: duration, ..zero }
2087            }
2088            StatOp::AddConnectAttemptsCount => StatCounters { connect_attempts_count: 1, ..zero },
2089            StatOp::AddConnectSuccessfulCount => {
2090                StatCounters { connect_successful_count: 1, ..zero }
2091            }
2092            StatOp::AddDisconnectCount(disconnect_source) => {
2093                if disconnect_source.has_roaming_cause() {
2094                    StatCounters { disconnect_count: 1, total_roam_disconnect_count: 1, ..zero }
2095                } else {
2096                    StatCounters { disconnect_count: 1, total_non_roam_disconnect_count: 1, ..zero }
2097                }
2098            }
2099            StatOp::AddPolicyRoamAttemptsCount(reasons) => {
2100                let mut counters = StatCounters { policy_roam_attempts_count: 1, ..zero };
2101                for reason in reasons {
2102                    let _ = counters.policy_roam_attempts_count_by_roam_reason.insert(reason, 1);
2103                }
2104                counters
2105            }
2106            StatOp::AddPolicyRoamSuccessfulCount(reasons) => {
2107                let mut counters = StatCounters { policy_roam_successful_count: 1, ..zero };
2108                for reason in reasons {
2109                    let _ = counters.policy_roam_successful_count_by_roam_reason.insert(reason, 1);
2110                }
2111                counters
2112            }
2113            StatOp::AddPolicyRoamDisconnectsCount => {
2114                StatCounters { policy_roam_disconnects_count: 1, ..zero }
2115            }
2116            StatOp::AddTxHighPacketDropDuration(duration) => {
2117                StatCounters { tx_high_packet_drop_duration: duration, ..zero }
2118            }
2119            StatOp::AddRxHighPacketDropDuration(duration) => {
2120                StatCounters { rx_high_packet_drop_duration: duration, ..zero }
2121            }
2122            StatOp::AddTxVeryHighPacketDropDuration(duration) => {
2123                StatCounters { tx_very_high_packet_drop_duration: duration, ..zero }
2124            }
2125            StatOp::AddRxVeryHighPacketDropDuration(duration) => {
2126                StatCounters { rx_very_high_packet_drop_duration: duration, ..zero }
2127            }
2128            StatOp::AddNoRxDuration(duration) => StatCounters { no_rx_duration: duration, ..zero },
2129        };
2130
2131        if addition != StatCounters::default() {
2132            self.last_1d_stats.lock().saturating_add(&addition);
2133            self.last_7d_stats.lock().saturating_add(&addition);
2134        }
2135    }
2136
2137    // Queue stat operation to be logged later. This allows the caller to control the timing of
2138    // when stats are logged. This ensures that various counters are not inconsistent with each
2139    // other because one is logged early and the other one later.
2140    fn queue_stat_op(&mut self, stat_op: StatOp) {
2141        self.stat_ops.push(stat_op);
2142    }
2143
2144    async fn log_queued_stats(&mut self) {
2145        while let Some(stat_op) = self.stat_ops.pop() {
2146            self.log_stat(stat_op).await;
2147        }
2148    }
2149
2150    async fn report_connect_result(
2151        &mut self,
2152        policy_connect_reason: Option<client::types::ConnectReason>,
2153        code: fidl_ieee80211::StatusCode,
2154        multiple_bss_candidates: bool,
2155        ap_state: &client::types::ApState,
2156        connect_start_time: Option<fasync::MonotonicInstant>,
2157    ) {
2158        self.log_establish_connection_cobalt_metrics(
2159            policy_connect_reason,
2160            code,
2161            multiple_bss_candidates,
2162            ap_state,
2163            connect_start_time,
2164        )
2165        .await;
2166
2167        *self.last_1d_detailed_stats.connect_attempts_status.entry(code).or_insert(0) += 1;
2168
2169        let is_multi_bss_dim = convert::convert_is_multi_bss(multiple_bss_candidates);
2170        self.last_1d_detailed_stats
2171            .connect_per_is_multi_bss
2172            .entry(is_multi_bss_dim)
2173            .or_default()
2174            .increment(code);
2175
2176        let security_type_dim = convert::convert_security_type(&ap_state.original().protection());
2177        self.last_1d_detailed_stats
2178            .connect_per_security_type
2179            .entry(security_type_dim)
2180            .or_default()
2181            .increment(code);
2182
2183        self.last_1d_detailed_stats
2184            .connect_per_primary_channel
2185            .entry(ap_state.tracked.channel.primary)
2186            .or_default()
2187            .increment(code);
2188
2189        let channel_band_dim = convert::convert_channel_band(ap_state.tracked.channel.band);
2190        self.last_1d_detailed_stats
2191            .connect_per_channel_band
2192            .entry(channel_band_dim)
2193            .or_default()
2194            .increment(code);
2195
2196        let rssi_bucket_dim = convert::convert_rssi_bucket(ap_state.tracked.signal.rssi_dbm);
2197        self.last_1d_detailed_stats
2198            .connect_per_rssi_bucket
2199            .entry(rssi_bucket_dim)
2200            .or_default()
2201            .increment(code);
2202
2203        let snr_bucket_dim = convert::convert_snr_bucket(ap_state.tracked.signal.snr_db);
2204        self.last_1d_detailed_stats
2205            .connect_per_snr_bucket
2206            .entry(snr_bucket_dim)
2207            .or_default()
2208            .increment(code);
2209    }
2210
2211    async fn log_daily_cobalt_metrics(&mut self) {
2212        self.log_daily_1d_cobalt_metrics().await;
2213        self.log_daily_7d_cobalt_metrics().await;
2214        self.log_daily_detailed_cobalt_metrics().await;
2215    }
2216
2217    async fn log_daily_1d_cobalt_metrics(&mut self) {
2218        let mut metric_events = vec![];
2219
2220        let c = self.last_1d_stats.lock().windowed_stat(None);
2221        let uptime_ratio = c.connected_duration.into_seconds() as f64
2222            / (c.connected_duration + c.adjusted_downtime()).into_seconds() as f64;
2223        if uptime_ratio.is_finite() {
2224            metric_events.push(MetricEvent {
2225                metric_id: metrics::CONNECTED_UPTIME_RATIO_METRIC_ID,
2226                event_codes: vec![],
2227                payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(uptime_ratio)),
2228            });
2229        }
2230
2231        let connected_dur_in_day = c.connected_duration.into_seconds() as f64 / (24 * 3600) as f64;
2232        let dpdc_ratio = c.disconnect_count as f64 / connected_dur_in_day;
2233        if dpdc_ratio.is_finite() {
2234            metric_events.push(MetricEvent {
2235                metric_id: metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID,
2236                event_codes: vec![],
2237                payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(dpdc_ratio)),
2238            });
2239        }
2240
2241        let roam_dpdc_ratio = c.policy_roam_disconnects_count as f64 / connected_dur_in_day;
2242        if roam_dpdc_ratio.is_finite() {
2243            metric_events.push(MetricEvent {
2244                metric_id: metrics::POLICY_ROAM_DISCONNECT_COUNT_PER_DAY_CONNECTED_METRIC_ID,
2245                event_codes: vec![],
2246                payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(roam_dpdc_ratio)),
2247            });
2248        }
2249
2250        let non_roam_dpdc_ratio = c.total_non_roam_disconnect_count as f64 / connected_dur_in_day;
2251        if non_roam_dpdc_ratio.is_finite() {
2252            metric_events.push(MetricEvent {
2253                metric_id: metrics::NON_ROAM_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID,
2254                event_codes: vec![],
2255                payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2256                    non_roam_dpdc_ratio,
2257                )),
2258            });
2259        }
2260
2261        let high_rx_drop_time_ratio = c.rx_high_packet_drop_duration.into_seconds() as f64
2262            / c.connected_duration.into_seconds() as f64;
2263        if high_rx_drop_time_ratio.is_finite() {
2264            metric_events.push(MetricEvent {
2265                metric_id: metrics::TIME_RATIO_WITH_HIGH_RX_PACKET_DROP_METRIC_ID,
2266                event_codes: vec![],
2267                payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2268                    high_rx_drop_time_ratio,
2269                )),
2270            });
2271        }
2272
2273        let high_tx_drop_time_ratio = c.tx_high_packet_drop_duration.into_seconds() as f64
2274            / c.connected_duration.into_seconds() as f64;
2275        if high_tx_drop_time_ratio.is_finite() {
2276            metric_events.push(MetricEvent {
2277                metric_id: metrics::TIME_RATIO_WITH_HIGH_TX_PACKET_DROP_METRIC_ID,
2278                event_codes: vec![],
2279                payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2280                    high_tx_drop_time_ratio,
2281                )),
2282            });
2283        }
2284
2285        let very_high_rx_drop_time_ratio = c.rx_very_high_packet_drop_duration.into_seconds()
2286            as f64
2287            / c.connected_duration.into_seconds() as f64;
2288        if very_high_rx_drop_time_ratio.is_finite() {
2289            metric_events.push(MetricEvent {
2290                metric_id: metrics::TIME_RATIO_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID,
2291                event_codes: vec![],
2292                payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2293                    very_high_rx_drop_time_ratio,
2294                )),
2295            });
2296        }
2297
2298        let very_high_tx_drop_time_ratio = c.tx_very_high_packet_drop_duration.into_seconds()
2299            as f64
2300            / c.connected_duration.into_seconds() as f64;
2301        if very_high_tx_drop_time_ratio.is_finite() {
2302            metric_events.push(MetricEvent {
2303                metric_id: metrics::TIME_RATIO_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID,
2304                event_codes: vec![],
2305                payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2306                    very_high_tx_drop_time_ratio,
2307                )),
2308            });
2309        }
2310
2311        let no_rx_time_ratio =
2312            c.no_rx_duration.into_seconds() as f64 / c.connected_duration.into_seconds() as f64;
2313        if no_rx_time_ratio.is_finite() {
2314            metric_events.push(MetricEvent {
2315                metric_id: metrics::TIME_RATIO_WITH_NO_RX_METRIC_ID,
2316                event_codes: vec![],
2317                payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2318                    no_rx_time_ratio,
2319                )),
2320            });
2321        }
2322
2323        let connection_success_rate = c.connection_success_rate();
2324        if connection_success_rate.is_finite() {
2325            metric_events.push(MetricEvent {
2326                metric_id: metrics::CONNECTION_SUCCESS_RATE_METRIC_ID,
2327                event_codes: vec![],
2328                payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2329                    connection_success_rate,
2330                )),
2331            });
2332        }
2333
2334        let policy_roam_success_rate = c.policy_roam_success_rate();
2335        if policy_roam_success_rate.is_finite() {
2336            metric_events.push(MetricEvent {
2337                metric_id: metrics::POLICY_ROAM_SUCCESS_RATE_METRIC_ID,
2338                event_codes: vec![],
2339                payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2340                    policy_roam_success_rate,
2341                )),
2342            });
2343        }
2344
2345        for reason in c.policy_roam_attempts_count_by_roam_reason.keys() {
2346            let success_rate = c.policy_roam_success_rate_by_roam_reason(reason);
2347            if success_rate.is_finite() {
2348                metric_events.push(MetricEvent {
2349                    metric_id: metrics::POLICY_ROAM_SUCCESS_RATE_BY_ROAM_REASON_METRIC_ID,
2350                    event_codes: vec![convert::convert_roam_reason_dimension(*reason) as u32],
2351                    payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2352                        success_rate,
2353                    )),
2354                });
2355            }
2356        }
2357
2358        self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2359            self.cobalt_proxy,
2360            &metric_events,
2361            "log_daily_1d_cobalt_metrics",
2362        ));
2363    }
2364
2365    async fn log_daily_7d_cobalt_metrics(&mut self) {
2366        let c = self.last_7d_stats.lock().windowed_stat(None);
2367        let connected_dur_in_day = c.connected_duration.into_seconds() as f64 / (24 * 3600) as f64;
2368        let dpdc_ratio = c.disconnect_count as f64 / connected_dur_in_day;
2369        #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
2370        if dpdc_ratio.is_finite() {
2371            let mut metric_events = vec![];
2372            metric_events.push(MetricEvent {
2373                metric_id: metrics::DISCONNECT_PER_DAY_CONNECTED_7D_METRIC_ID,
2374                event_codes: vec![],
2375                payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(dpdc_ratio)),
2376            });
2377
2378            self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2379                self.cobalt_proxy,
2380                &metric_events,
2381                "log_daily_7d_cobalt_metrics",
2382            ));
2383        }
2384    }
2385
2386    async fn log_daily_detailed_cobalt_metrics(&mut self) {
2387        let mut metric_events = vec![];
2388
2389        let c = self.last_1d_stats.lock().windowed_stat(None);
2390        if c.connection_success_rate().is_finite() {
2391            let device_low_connection_success =
2392                c.connection_success_rate() < DEVICE_LOW_CONNECTION_SUCCESS_RATE_THRESHOLD;
2393            for (status_code, count) in &self.last_1d_detailed_stats.connect_attempts_status {
2394                metric_events.push(MetricEvent {
2395                    metric_id: if device_low_connection_success {
2396                        metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID
2397                    } else {
2398                        metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID
2399                    },
2400                    event_codes: vec![(*status_code).into_primitive() as u32],
2401                    payload: MetricEventPayload::Count(*count),
2402                });
2403            }
2404
2405            for (is_multi_bss_dim, counters) in
2406                &self.last_1d_detailed_stats.connect_per_is_multi_bss
2407            {
2408                let success_rate = counters.success as f64 / counters.total as f64;
2409                metric_events.push(MetricEvent {
2410                    metric_id:
2411                        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
2412                    event_codes: vec![*is_multi_bss_dim as u32],
2413                    payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2414                        success_rate,
2415                    )),
2416                });
2417            }
2418        }
2419
2420        self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2421            self.cobalt_proxy,
2422            &metric_events,
2423            "log_daily_detailed_cobalt_metrics",
2424        ));
2425    }
2426
2427    async fn handle_hr_passed(&mut self) {
2428        self.log_hourly_fleetwise_quality_cobalt_metrics().await;
2429
2430        self.hr_tick = (self.hr_tick + 1) % 24;
2431        self.last_1d_stats.lock().slide_window();
2432        if self.hr_tick == 0 {
2433            self.last_7d_stats.lock().slide_window();
2434            self.last_1d_detailed_stats = DailyDetailedStats::new();
2435        }
2436
2437        self.log_hourly_rssi_histogram_metrics().await;
2438    }
2439
2440    // Send out the RSSI and RSSI velocity metrics that have been collected over the last hour.
2441    async fn log_hourly_rssi_histogram_metrics(&mut self) {
2442        let rssi_buckets: Vec<_> = self.rssi_hist.values().copied().collect();
2443        self.throttled_error_logger.throttle_error(log_cobalt!(
2444            self.cobalt_proxy,
2445            log_integer_histogram,
2446            metrics::CONNECTION_RSSI_METRIC_ID,
2447            &rssi_buckets,
2448            &[],
2449        ));
2450        self.rssi_hist.clear();
2451
2452        let velocity_buckets: Vec<_> = self.rssi_velocity_hist.values().copied().collect();
2453        self.throttled_error_logger.throttle_error(log_cobalt!(
2454            self.cobalt_proxy,
2455            log_integer_histogram,
2456            metrics::RSSI_VELOCITY_METRIC_ID,
2457            &velocity_buckets,
2458            &[],
2459        ));
2460        self.rssi_velocity_hist.clear();
2461    }
2462
2463    async fn log_chip_power_up_failure(&mut self) {
2464        self.throttled_error_logger.throttle_error(log_cobalt!(
2465            self.cobalt_proxy,
2466            log_occurrence,
2467            metrics::CHIP_POWER_UP_FAILURE_METRIC_ID,
2468            1,
2469            &[],
2470        ));
2471    }
2472
2473    async fn log_hourly_fleetwise_quality_cobalt_metrics(&mut self) {
2474        let mut metric_events = vec![];
2475
2476        // Get stats from the last hour
2477        let c = self.last_1d_stats.lock().windowed_stat(Some(1));
2478        let total_wlan_uptime = c.connected_duration + c.adjusted_downtime();
2479
2480        // Log the durations calculated in the last hour
2481        metric_events.push(MetricEvent {
2482            metric_id: metrics::TOTAL_WLAN_UPTIME_NEAR_SAVED_NETWORK_METRIC_ID,
2483            event_codes: vec![],
2484            payload: MetricEventPayload::IntegerValue(total_wlan_uptime.into_micros()),
2485        });
2486        metric_events.push(MetricEvent {
2487            metric_id: metrics::TOTAL_CONNECTED_UPTIME_METRIC_ID,
2488            event_codes: vec![],
2489            payload: MetricEventPayload::IntegerValue(c.connected_duration.into_micros()),
2490        });
2491        metric_events.push(MetricEvent {
2492            metric_id: metrics::TOTAL_TIME_WITH_HIGH_RX_PACKET_DROP_METRIC_ID,
2493            event_codes: vec![],
2494            payload: MetricEventPayload::IntegerValue(c.rx_high_packet_drop_duration.into_micros()),
2495        });
2496        metric_events.push(MetricEvent {
2497            metric_id: metrics::TOTAL_TIME_WITH_HIGH_TX_PACKET_DROP_METRIC_ID,
2498            event_codes: vec![],
2499            payload: MetricEventPayload::IntegerValue(c.tx_high_packet_drop_duration.into_micros()),
2500        });
2501        metric_events.push(MetricEvent {
2502            metric_id: metrics::TOTAL_TIME_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID,
2503            event_codes: vec![],
2504            payload: MetricEventPayload::IntegerValue(
2505                c.rx_very_high_packet_drop_duration.into_micros(),
2506            ),
2507        });
2508        metric_events.push(MetricEvent {
2509            metric_id: metrics::TOTAL_TIME_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID,
2510            event_codes: vec![],
2511            payload: MetricEventPayload::IntegerValue(
2512                c.tx_very_high_packet_drop_duration.into_micros(),
2513            ),
2514        });
2515        metric_events.push(MetricEvent {
2516            metric_id: metrics::TOTAL_TIME_WITH_NO_RX_METRIC_ID,
2517            event_codes: vec![],
2518            payload: MetricEventPayload::IntegerValue(c.no_rx_duration.into_micros()),
2519        });
2520
2521        self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2522            self.cobalt_proxy,
2523            &metric_events,
2524            "log_hourly_fleetwise_quality_cobalt_metrics",
2525        ));
2526    }
2527
2528    async fn log_disconnect_cobalt_metrics(
2529        &mut self,
2530        disconnect_info: &DisconnectInfo,
2531        multiple_bss_candidates: bool,
2532    ) {
2533        let mut metric_events = vec![];
2534        let policy_disconnect_reason_dim = {
2535            use metrics::PolicyDisconnectionMigratedMetricDimensionReason::*;
2536            match &disconnect_info.disconnect_source {
2537                fidl_sme::DisconnectSource::User(reason) => match reason {
2538                    fidl_sme::UserDisconnectReason::Unknown => Unknown,
2539                    fidl_sme::UserDisconnectReason::FailedToConnect => FailedToConnect,
2540                    fidl_sme::UserDisconnectReason::FidlConnectRequest => FidlConnectRequest,
2541                    fidl_sme::UserDisconnectReason::FidlStopClientConnectionsRequest => {
2542                        FidlStopClientConnectionsRequest
2543                    }
2544                    fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch => {
2545                        ProactiveNetworkSwitch
2546                    }
2547                    fidl_sme::UserDisconnectReason::DisconnectDetectedFromSme => {
2548                        DisconnectDetectedFromSme
2549                    }
2550                    fidl_sme::UserDisconnectReason::RegulatoryRegionChange => {
2551                        RegulatoryRegionChange
2552                    }
2553                    fidl_sme::UserDisconnectReason::Startup => Startup,
2554                    fidl_sme::UserDisconnectReason::NetworkUnsaved => NetworkUnsaved,
2555                    fidl_sme::UserDisconnectReason::NetworkConfigUpdated => NetworkConfigUpdated,
2556                    fidl_sme::UserDisconnectReason::WlanstackUnitTesting
2557                    | fidl_sme::UserDisconnectReason::WlanSmeUnitTesting
2558                    | fidl_sme::UserDisconnectReason::WlanServiceUtilTesting
2559                    | fidl_sme::UserDisconnectReason::WlanDevTool
2560                    | fidl_sme::UserDisconnectReason::Recovery => Unknown,
2561                },
2562                fidl_sme::DisconnectSource::Ap(..) | fidl_sme::DisconnectSource::Mlme(..) => {
2563                    DisconnectDetectedFromSme
2564                }
2565            }
2566        };
2567        metric_events.push(MetricEvent {
2568            metric_id: metrics::POLICY_DISCONNECTION_MIGRATED_METRIC_ID,
2569            event_codes: vec![policy_disconnect_reason_dim as u32],
2570            payload: MetricEventPayload::Count(1),
2571        });
2572
2573        let device_uptime_dim = {
2574            use metrics::DisconnectBreakdownByDeviceUptimeMetricDimensionDeviceUptime::*;
2575            match fasync::MonotonicInstant::now() - fasync::MonotonicInstant::from_nanos(0) {
2576                x if x < zx::MonotonicDuration::from_hours(1) => LessThan1Hour,
2577                x if x < zx::MonotonicDuration::from_hours(3) => LessThan3Hours,
2578                x if x < zx::MonotonicDuration::from_hours(12) => LessThan12Hours,
2579                x if x < zx::MonotonicDuration::from_hours(24) => LessThan1Day,
2580                x if x < zx::MonotonicDuration::from_hours(48) => LessThan2Days,
2581                _ => AtLeast2Days,
2582            }
2583        };
2584        metric_events.push(MetricEvent {
2585            metric_id: metrics::DISCONNECT_BREAKDOWN_BY_DEVICE_UPTIME_METRIC_ID,
2586            event_codes: vec![device_uptime_dim as u32],
2587            payload: MetricEventPayload::Count(1),
2588        });
2589
2590        let connected_duration_dim = {
2591            use metrics::DisconnectBreakdownByConnectedDurationMetricDimensionConnectedDuration::*;
2592            match disconnect_info.connected_duration {
2593                x if x < zx::MonotonicDuration::from_seconds(30) => LessThan30Seconds,
2594                x if x < zx::MonotonicDuration::from_minutes(5) => LessThan5Minutes,
2595                x if x < zx::MonotonicDuration::from_hours(1) => LessThan1Hour,
2596                x if x < zx::MonotonicDuration::from_hours(6) => LessThan6Hours,
2597                x if x < zx::MonotonicDuration::from_hours(24) => LessThan24Hours,
2598                _ => AtLeast24Hours,
2599            }
2600        };
2601        metric_events.push(MetricEvent {
2602            metric_id: metrics::DISCONNECT_BREAKDOWN_BY_CONNECTED_DURATION_METRIC_ID,
2603            event_codes: vec![connected_duration_dim as u32],
2604            payload: MetricEventPayload::Count(1),
2605        });
2606
2607        metric_events.push(MetricEvent {
2608            metric_id: metrics::DISCONNECT_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
2609            event_codes: vec![disconnect_info.ap_state.tracked.channel.primary as u32],
2610            payload: MetricEventPayload::Count(1),
2611        });
2612        let channel_band_dim =
2613            convert::convert_channel_band(disconnect_info.ap_state.tracked.channel.band);
2614        metric_events.push(MetricEvent {
2615            metric_id: metrics::DISCONNECT_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
2616            event_codes: vec![channel_band_dim as u32],
2617            payload: MetricEventPayload::Count(1),
2618        });
2619        let is_multi_bss_dim = convert::convert_is_multi_bss(multiple_bss_candidates);
2620        metric_events.push(MetricEvent {
2621            metric_id: metrics::DISCONNECT_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
2622            event_codes: vec![is_multi_bss_dim as u32],
2623            payload: MetricEventPayload::Count(1),
2624        });
2625        let security_type_dim =
2626            convert::convert_security_type(&disconnect_info.ap_state.original().protection());
2627        metric_events.push(MetricEvent {
2628            metric_id: metrics::DISCONNECT_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
2629            event_codes: vec![security_type_dim as u32],
2630            payload: MetricEventPayload::Count(1),
2631        });
2632
2633        // Log only non-roaming disconnects. Roaming disconnect counts are handled in the roam
2634        //result event, to differentiate a successful roam from a true disconnect.
2635        let duration_minutes = disconnect_info.connected_duration.into_minutes();
2636        if !disconnect_info.disconnect_source.has_roaming_cause() {
2637            metric_events.push(MetricEvent {
2638                metric_id: metrics::CONNECTED_DURATION_BEFORE_NON_ROAM_DISCONNECT_METRIC_ID,
2639                event_codes: vec![],
2640                payload: MetricEventPayload::IntegerValue(duration_minutes),
2641            });
2642            // Daily device occurrence count
2643            metric_events.push(MetricEvent {
2644                metric_id: metrics::NON_ROAM_DISCONNECT_COUNTS_METRIC_ID,
2645                event_codes: vec![],
2646                payload: MetricEventPayload::Count(1),
2647            });
2648            // Fleetwide occurrence count
2649            metric_events.push(MetricEvent {
2650                metric_id: metrics::TOTAL_NON_ROAM_DISCONNECT_COUNT_METRIC_ID,
2651                event_codes: vec![],
2652                payload: MetricEventPayload::Count(1),
2653            })
2654        }
2655
2656        metric_events.push(MetricEvent {
2657            metric_id: metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID,
2658            event_codes: vec![],
2659            payload: MetricEventPayload::IntegerValue(duration_minutes),
2660        });
2661
2662        metric_events.push(MetricEvent {
2663            metric_id: metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID,
2664            event_codes: vec![],
2665            payload: MetricEventPayload::Count(1),
2666        });
2667
2668        self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2669            self.cobalt_proxy,
2670            &metric_events,
2671            "log_disconnect_cobalt_metrics",
2672        ));
2673    }
2674
2675    async fn log_active_scan_requested_cobalt_metrics(&mut self, num_ssids_requested: usize) {
2676        use metrics::ActiveScanRequestedForNetworkSelectionMigratedMetricDimensionActiveScanSsidsRequested as ActiveScanSsidsRequested;
2677        let active_scan_ssids_requested_dim = match num_ssids_requested {
2678            0 => ActiveScanSsidsRequested::Zero,
2679            1 => ActiveScanSsidsRequested::One,
2680            2..=4 => ActiveScanSsidsRequested::TwoToFour,
2681            5..=10 => ActiveScanSsidsRequested::FiveToTen,
2682            11..=20 => ActiveScanSsidsRequested::ElevenToTwenty,
2683            21..=50 => ActiveScanSsidsRequested::TwentyOneToFifty,
2684            51..=100 => ActiveScanSsidsRequested::FiftyOneToOneHundred,
2685            101.. => ActiveScanSsidsRequested::OneHundredAndOneOrMore,
2686        };
2687        self.throttled_error_logger.throttle_error(log_cobalt!(
2688            self.cobalt_proxy,
2689            log_occurrence,
2690            metrics::ACTIVE_SCAN_REQUESTED_FOR_NETWORK_SELECTION_MIGRATED_METRIC_ID,
2691            1,
2692            &[active_scan_ssids_requested_dim as u32],
2693        ));
2694    }
2695
2696    async fn log_active_scan_requested_via_api_cobalt_metrics(
2697        &mut self,
2698        num_ssids_requested: usize,
2699    ) {
2700        use metrics::ActiveScanRequestedForPolicyApiMetricDimensionActiveScanSsidsRequested as ActiveScanSsidsRequested;
2701        let active_scan_ssids_requested_dim = match num_ssids_requested {
2702            0 => ActiveScanSsidsRequested::Zero,
2703            1 => ActiveScanSsidsRequested::One,
2704            2..=4 => ActiveScanSsidsRequested::TwoToFour,
2705            5..=10 => ActiveScanSsidsRequested::FiveToTen,
2706            11..=20 => ActiveScanSsidsRequested::ElevenToTwenty,
2707            21..=50 => ActiveScanSsidsRequested::TwentyOneToFifty,
2708            51..=100 => ActiveScanSsidsRequested::FiftyOneToOneHundred,
2709            101.. => ActiveScanSsidsRequested::OneHundredAndOneOrMore,
2710        };
2711        self.throttled_error_logger.throttle_error(log_cobalt!(
2712            self.cobalt_proxy,
2713            log_occurrence,
2714            metrics::ACTIVE_SCAN_REQUESTED_FOR_POLICY_API_METRIC_ID,
2715            1,
2716            &[active_scan_ssids_requested_dim as u32],
2717        ));
2718    }
2719
2720    async fn log_saved_network_counts(
2721        &mut self,
2722        saved_network_count: usize,
2723        config_count_per_saved_network: Vec<usize>,
2724    ) {
2725        let mut metric_events = vec![];
2726
2727        // Count the total number of saved networks
2728        use metrics::SavedNetworksMigratedMetricDimensionSavedNetworks as SavedNetworksCount;
2729        let num_networks = match saved_network_count {
2730            0 => SavedNetworksCount::Zero,
2731            1 => SavedNetworksCount::One,
2732            2..=4 => SavedNetworksCount::TwoToFour,
2733            5..=40 => SavedNetworksCount::FiveToForty,
2734            41..=500 => SavedNetworksCount::FortyToFiveHundred,
2735            501.. => SavedNetworksCount::FiveHundredAndOneOrMore,
2736        };
2737        metric_events.push(MetricEvent {
2738            metric_id: metrics::SAVED_NETWORKS_MIGRATED_METRIC_ID,
2739            event_codes: vec![num_networks as u32],
2740            payload: MetricEventPayload::Count(1),
2741        });
2742
2743        // Count the number of configs for each saved network
2744        use metrics::SavedConfigurationsForSavedNetworkMigratedMetricDimensionSavedConfigurations as ConfigCountDimension;
2745        for config_count in config_count_per_saved_network {
2746            let num_configs = match config_count {
2747                0 => ConfigCountDimension::Zero,
2748                1 => ConfigCountDimension::One,
2749                2..=4 => ConfigCountDimension::TwoToFour,
2750                5..=40 => ConfigCountDimension::FiveToForty,
2751                41..=500 => ConfigCountDimension::FortyToFiveHundred,
2752                501.. => ConfigCountDimension::FiveHundredAndOneOrMore,
2753            };
2754            metric_events.push(MetricEvent {
2755                metric_id: metrics::SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_MIGRATED_METRIC_ID,
2756                event_codes: vec![num_configs as u32],
2757                payload: MetricEventPayload::Count(1),
2758            });
2759        }
2760
2761        self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2762            self.cobalt_proxy,
2763            &metric_events,
2764            "log_saved_network_counts",
2765        ));
2766    }
2767
2768    async fn log_network_selection_scan_interval(
2769        &mut self,
2770        time_since_last_scan: zx::MonotonicDuration,
2771    ) {
2772        self.throttled_error_logger.throttle_error(log_cobalt!(
2773            self.cobalt_proxy,
2774            log_integer,
2775            metrics::LAST_SCAN_AGE_WHEN_SCAN_REQUESTED_MIGRATED_METRIC_ID,
2776            time_since_last_scan.into_micros(),
2777            &[],
2778        ));
2779    }
2780
2781    async fn log_connection_selection_scan_results(
2782        &mut self,
2783        saved_network_count: usize,
2784        bss_count_per_saved_network: Vec<usize>,
2785        saved_network_count_found_by_active_scan: usize,
2786    ) {
2787        let mut metric_events = vec![];
2788
2789        use metrics::SavedNetworkInScanResultMigratedMetricDimensionBssCount as BssCount;
2790        for bss_count in bss_count_per_saved_network {
2791            // Record how many BSSs are visible in the scan results for this saved network.
2792            let bss_count_metric = match bss_count {
2793                0 => BssCount::Zero, // The ::Zero enum exists, but we shouldn't get a scan result with no BSS
2794                1 => BssCount::One,
2795                2..=4 => BssCount::TwoToFour,
2796                5..=10 => BssCount::FiveToTen,
2797                11..=20 => BssCount::ElevenToTwenty,
2798                21.. => BssCount::TwentyOneOrMore,
2799            };
2800            metric_events.push(MetricEvent {
2801                metric_id: metrics::SAVED_NETWORK_IN_SCAN_RESULT_MIGRATED_METRIC_ID,
2802                event_codes: vec![bss_count_metric as u32],
2803                payload: MetricEventPayload::Count(1),
2804            });
2805        }
2806
2807        use metrics::ScanResultsReceivedMigratedMetricDimensionSavedNetworksCount as SavedNetworkCount;
2808        let saved_network_count_metric = match saved_network_count {
2809            0 => SavedNetworkCount::Zero,
2810            1 => SavedNetworkCount::One,
2811            2..=4 => SavedNetworkCount::TwoToFour,
2812            5..=20 => SavedNetworkCount::FiveToTwenty,
2813            21..=40 => SavedNetworkCount::TwentyOneToForty,
2814            41.. => SavedNetworkCount::FortyOneOrMore,
2815        };
2816        metric_events.push(MetricEvent {
2817            metric_id: metrics::SCAN_RESULTS_RECEIVED_MIGRATED_METRIC_ID,
2818            event_codes: vec![saved_network_count_metric as u32],
2819            payload: MetricEventPayload::Count(1),
2820        });
2821
2822        use metrics::SavedNetworkInScanResultWithActiveScanMigratedMetricDimensionActiveScanSsidsObserved as ActiveScanSsidsObserved;
2823        let actively_scanned_networks_metrics = match saved_network_count_found_by_active_scan {
2824            0 => ActiveScanSsidsObserved::Zero,
2825            1 => ActiveScanSsidsObserved::One,
2826            2..=4 => ActiveScanSsidsObserved::TwoToFour,
2827            5..=10 => ActiveScanSsidsObserved::FiveToTen,
2828            11..=20 => ActiveScanSsidsObserved::ElevenToTwenty,
2829            21..=50 => ActiveScanSsidsObserved::TwentyOneToFifty,
2830            51..=100 => ActiveScanSsidsObserved::FiftyOneToOneHundred,
2831            101.. => ActiveScanSsidsObserved::OneHundredAndOneOrMore,
2832        };
2833        metric_events.push(MetricEvent {
2834            metric_id: metrics::SAVED_NETWORK_IN_SCAN_RESULT_WITH_ACTIVE_SCAN_MIGRATED_METRIC_ID,
2835            event_codes: vec![actively_scanned_networks_metrics as u32],
2836            payload: MetricEventPayload::Count(1),
2837        });
2838
2839        self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2840            self.cobalt_proxy,
2841            &metric_events,
2842            "log_connection_selection_scan_results",
2843        ));
2844    }
2845
2846    async fn log_establish_connection_cobalt_metrics(
2847        &mut self,
2848        policy_connect_reason: Option<client::types::ConnectReason>,
2849        code: fidl_ieee80211::StatusCode,
2850        multiple_bss_candidates: bool,
2851        ap_state: &client::types::ApState,
2852        connect_start_time: Option<fasync::MonotonicInstant>,
2853    ) {
2854        let metric_events = self.build_establish_connection_cobalt_metrics(
2855            policy_connect_reason,
2856            code,
2857            multiple_bss_candidates,
2858            ap_state,
2859            connect_start_time,
2860        );
2861        self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2862            self.cobalt_proxy,
2863            &metric_events,
2864            "log_establish_connection_cobalt_metrics",
2865        ));
2866    }
2867
2868    fn build_establish_connection_cobalt_metrics(
2869        &mut self,
2870        policy_connect_reason: Option<client::types::ConnectReason>,
2871        code: fidl_ieee80211::StatusCode,
2872        multiple_bss_candidates: bool,
2873        ap_state: &client::types::ApState,
2874        connect_start_time: Option<fasync::MonotonicInstant>,
2875    ) -> Vec<MetricEvent> {
2876        let mut metric_events = vec![];
2877        if let Some(policy_connect_reason) = policy_connect_reason {
2878            metric_events.push(MetricEvent {
2879                metric_id: metrics::POLICY_CONNECTION_ATTEMPT_MIGRATED_METRIC_ID,
2880                event_codes: vec![policy_connect_reason as u32],
2881                payload: MetricEventPayload::Count(1),
2882            });
2883
2884            // Also log non-retry connect attempts without dimension
2885            match policy_connect_reason {
2886                metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::FidlConnectRequest
2887                | metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::ProactiveNetworkSwitch
2888                | metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::IdleInterfaceAutoconnect
2889                | metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::NewSavedNetworkAutoconnect => {
2890                    metric_events.push(MetricEvent {
2891                        metric_id: metrics::POLICY_CONNECTION_ATTEMPTS_METRIC_ID,
2892                        event_codes: vec![],
2893                        payload: MetricEventPayload::Count(1),
2894                    });
2895                }
2896                metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::RetryAfterDisconnectDetected
2897                | metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::RetryAfterFailedConnectAttempt
2898                | metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::RegulatoryChangeReconnect => (),
2899            }
2900        }
2901
2902        if code != fidl_ieee80211::StatusCode::Success {
2903            return metric_events;
2904        }
2905
2906        match connect_start_time {
2907            Some(start_time) => {
2908                let user_wait_time = fasync::MonotonicInstant::now() - start_time;
2909                let user_wait_time_dim = convert::convert_user_wait_time(user_wait_time);
2910                metric_events.push(MetricEvent {
2911                    metric_id: metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID,
2912                    event_codes: vec![user_wait_time_dim as u32],
2913                    payload: MetricEventPayload::Count(1),
2914                });
2915            }
2916            None => warn!(
2917                "Metric for user wait time on connect is not logged because \
2918                 the start time is not populated"
2919            ),
2920        }
2921
2922        let is_multi_bss_dim = convert::convert_is_multi_bss(multiple_bss_candidates);
2923        metric_events.push(MetricEvent {
2924            metric_id: metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
2925            event_codes: vec![is_multi_bss_dim as u32],
2926            payload: MetricEventPayload::Count(1),
2927        });
2928
2929        let security_type_dim = convert::convert_security_type(&ap_state.original().protection());
2930        metric_events.push(MetricEvent {
2931            metric_id: metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
2932            event_codes: vec![security_type_dim as u32],
2933            payload: MetricEventPayload::Count(1),
2934        });
2935
2936        metric_events.push(MetricEvent {
2937            metric_id: metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
2938            event_codes: vec![ap_state.tracked.channel.primary as u32],
2939            payload: MetricEventPayload::Count(1),
2940        });
2941
2942        let channel_band_dim = convert::convert_channel_band(ap_state.tracked.channel.band);
2943        metric_events.push(MetricEvent {
2944            metric_id: metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
2945            event_codes: vec![channel_band_dim as u32],
2946            payload: MetricEventPayload::Count(1),
2947        });
2948
2949        metric_events
2950    }
2951
2952    async fn log_downtime_cobalt_metrics(
2953        &mut self,
2954        downtime: zx::MonotonicDuration,
2955        disconnect_info: &DisconnectInfo,
2956    ) {
2957        let disconnect_source_dim =
2958            convert::convert_disconnect_source(&disconnect_info.disconnect_source);
2959        self.throttled_error_logger.throttle_error(log_cobalt!(
2960            self.cobalt_proxy,
2961            log_integer,
2962            metrics::DOWNTIME_BREAKDOWN_BY_DISCONNECT_REASON_METRIC_ID,
2963            downtime.into_micros(),
2964            &[
2965                disconnect_info.disconnect_source.cobalt_reason_code() as u32,
2966                disconnect_source_dim as u32
2967            ],
2968        ));
2969    }
2970
2971    async fn log_reconnect_cobalt_metrics(
2972        &mut self,
2973        reconnect_duration: zx::MonotonicDuration,
2974        disconnect_reason: fidl_sme::DisconnectSource,
2975    ) {
2976        let mut metric_events = vec![];
2977
2978        // Log the reconnect time for non-roaming disconnects. Roaming reconnect
2979        // times are logged in the roam result event, as they are different than true disconnects.
2980        if !disconnect_reason.has_roaming_cause() {
2981            metric_events.push(MetricEvent {
2982                metric_id: metrics::NON_ROAM_RECONNECT_DURATION_METRIC_ID,
2983                event_codes: vec![],
2984                payload: MetricEventPayload::IntegerValue(reconnect_duration.into_micros()),
2985            });
2986        }
2987
2988        self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2989            self.cobalt_proxy,
2990            &metric_events,
2991            "log_reconnect_cobalt_metrics",
2992        ));
2993    }
2994
2995    /// Metrics to log when device first connects to an AP, and periodically afterward
2996    /// (at least once a day) if the device is still connected to the AP.
2997    async fn log_device_connected_cobalt_metrics(
2998        &mut self,
2999        multiple_bss_candidates: bool,
3000        _ap_state: &client::types::ApState,
3001        network_is_likely_hidden: bool,
3002    ) {
3003        let mut metric_events = vec![];
3004
3005        let is_multi_bss_dim = convert::convert_is_multi_bss(multiple_bss_candidates);
3006        metric_events.push(MetricEvent {
3007            metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
3008            event_codes: vec![is_multi_bss_dim as u32],
3009            payload: MetricEventPayload::Count(1),
3010        });
3011
3012        if network_is_likely_hidden {
3013            metric_events.push(MetricEvent {
3014                metric_id: metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID,
3015                event_codes: vec![],
3016                payload: MetricEventPayload::Count(1),
3017            });
3018        }
3019
3020        self.throttled_error_logger.throttle_error(log_cobalt_batch!(
3021            self.cobalt_proxy,
3022            &metric_events,
3023            "log_device_connected_cobalt_metrics",
3024        ));
3025    }
3026
3027    async fn log_policy_roam_scan_metrics(&mut self, reasons: Vec<RoamReason>) {
3028        self.throttled_error_logger.throttle_error(log_cobalt!(
3029            self.cobalt_proxy,
3030            log_occurrence,
3031            metrics::POLICY_ROAM_SCAN_COUNT_METRIC_ID,
3032            1,
3033            &[],
3034        ));
3035        for reason in reasons {
3036            self.throttled_error_logger.throttle_error(log_cobalt!(
3037                self.cobalt_proxy,
3038                log_occurrence,
3039                metrics::POLICY_ROAM_SCAN_COUNT_BY_ROAM_REASON_METRIC_ID,
3040                1,
3041                &[convert::convert_roam_reason_dimension(reason) as u32],
3042            ));
3043        }
3044    }
3045
3046    async fn log_policy_roam_attempt_metrics(
3047        &mut self,
3048        request: PolicyRoamRequest,
3049        connected_duration: zx::MonotonicDuration,
3050    ) {
3051        self.throttled_error_logger.throttle_error(log_cobalt!(
3052            self.cobalt_proxy,
3053            log_occurrence,
3054            metrics::POLICY_ROAM_ATTEMPT_COUNT_METRIC_ID,
3055            1,
3056            &[],
3057        ));
3058        for reason in &request.reasons {
3059            self.throttled_error_logger.throttle_error(log_cobalt!(
3060                self.cobalt_proxy,
3061                log_occurrence,
3062                metrics::POLICY_ROAM_ATTEMPT_COUNT_BY_ROAM_REASON_METRIC_ID,
3063                1,
3064                &[convert::convert_roam_reason_dimension(*reason) as u32],
3065            ));
3066            self.throttled_error_logger.throttle_error(log_cobalt!(
3067                self.cobalt_proxy,
3068                log_integer,
3069                metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
3070                connected_duration.into_minutes(),
3071                &[convert::convert_roam_reason_dimension(*reason) as u32],
3072            ));
3073        }
3074        self.log_stat(StatOp::AddPolicyRoamAttemptsCount(request.reasons)).await;
3075    }
3076
3077    async fn log_roam_result_metrics(
3078        &mut self,
3079        result: fidl_sme::RoamResult,
3080        updated_ap_state: client::types::ApState,
3081        original_ap_state: Box<client::types::ApState>,
3082        request: Box<PolicyRoamRequest>,
3083        request_time: fasync::MonotonicInstant,
3084        result_time: fasync::MonotonicInstant,
3085    ) {
3086        // Log the detailed roam attempt metric after completion, because it requires knowledge of the
3087        // outcome.
3088        let was_roam_successful = if result.status_code == fidl_ieee80211::StatusCode::Success {
3089            metrics::PolicyRoamAttemptCountDetailedMetricDimensionWasRoamSuccessful::Yes as u32
3090        } else {
3091            metrics::PolicyRoamAttemptCountDetailedMetricDimensionWasRoamSuccessful::No as u32
3092        };
3093        let ghz_band_transition = convert::get_ghz_band_transition(
3094            &original_ap_state.tracked.channel,
3095            &request.candidate.bss.channel,
3096        ) as u32;
3097        for reason in &request.reasons {
3098            // TODO(https://fxbug.dev/455916035): Stop logging to this metric when
3099            // it's deleted during the next metric maintenance.
3100            self.throttled_error_logger.throttle_error(log_cobalt!(
3101                self.cobalt_proxy,
3102                log_occurrence,
3103                metrics::POLICY_ROAM_ATTEMPT_COUNT_DETAILED_METRIC_ID,
3104                1,
3105                &[
3106                    convert::convert_roam_reason_dimension(*reason) as u32,
3107                    was_roam_successful,
3108                    ghz_band_transition,
3109                    0, // Deprecated dfs_channel_transition
3110                ],
3111            ));
3112            self.throttled_error_logger.throttle_error(log_cobalt!(
3113                self.cobalt_proxy,
3114                log_occurrence,
3115                metrics::POLICY_ROAM_ATTEMPT_COUNT_DETAILED_2_METRIC_ID,
3116                1,
3117                &[
3118                    convert::convert_roam_reason_dimension(*reason) as u32,
3119                    was_roam_successful,
3120                    ghz_band_transition,
3121                ],
3122            ));
3123        }
3124
3125        // Exit early if the original association maintained.
3126        if result.original_association_maintained {
3127            return;
3128        }
3129
3130        // Log disconnects, since the device left the original AP (for either roam success, or
3131        // failure when the original association was not maintained).
3132        // Log a policy roam disconnect.
3133        self.throttled_error_logger.throttle_error(log_cobalt!(
3134            self.cobalt_proxy,
3135            log_occurrence,
3136            metrics::POLICY_ROAM_DISCONNECT_COUNT_METRIC_ID,
3137            1,
3138            &[],
3139        ));
3140        // Add to the policy roam disconnect count stat counter
3141        self.log_stat(StatOp::AddPolicyRoamDisconnectsCount).await;
3142        // Log with roam reasons
3143        for reason in &request.reasons {
3144            self.throttled_error_logger.throttle_error(log_cobalt!(
3145                self.cobalt_proxy,
3146                log_occurrence,
3147                metrics::POLICY_ROAM_DISCONNECT_COUNT_BY_ROAM_REASON_METRIC_ID,
3148                1,
3149                &[convert::convert_roam_reason_dimension(*reason) as u32],
3150            ));
3151        }
3152        // Log a total (policy or firmware initiated) roam disconnect.
3153        self.throttled_error_logger.throttle_error(log_cobalt!(
3154            self.cobalt_proxy,
3155            log_occurrence,
3156            metrics::TOTAL_ROAM_DISCONNECT_COUNT_METRIC_ID,
3157            1,
3158            &[],
3159        ));
3160
3161        if result.status_code == fidl_ieee80211::StatusCode::Success {
3162            self.log_stat(StatOp::AddPolicyRoamSuccessfulCount(request.reasons.clone())).await;
3163            self.throttled_error_logger.throttle_error(log_cobalt!(
3164                self.cobalt_proxy,
3165                log_integer,
3166                metrics::POLICY_ROAM_RECONNECT_DURATION_METRIC_ID,
3167                fasync::MonotonicDuration::from(result_time - request_time).into_micros(),
3168                &[],
3169            ));
3170
3171            // Log the RSSI delta from before/after successful roam.
3172            let rssi_delta = (updated_ap_state.tracked.signal.rssi_dbm)
3173                .saturating_sub(original_ap_state.tracked.signal.rssi_dbm);
3174            for reason in &request.reasons {
3175                self.throttled_error_logger.throttle_error(log_cobalt!(
3176                    self.cobalt_proxy,
3177                    log_integer,
3178                    metrics::POLICY_ROAM_TRANSITION_RSSI_DELTA_BY_ROAM_REASON_METRIC_ID,
3179                    convert::calculate_rssi_delta_bucket(rssi_delta),
3180                    &[convert::convert_roam_reason_dimension(*reason) as u32],
3181                ))
3182            }
3183        }
3184    }
3185
3186    /// Log metrics that will be used to analyze when roaming would happen before roams are
3187    /// enabled.
3188    async fn log_would_roam_connect(&mut self) {
3189        self.throttled_error_logger.throttle_error(log_cobalt!(
3190            self.cobalt_proxy,
3191            log_occurrence,
3192            metrics::POLICY_ROAM_ATTEMPT_COUNT_METRIC_ID,
3193            1,
3194            &[],
3195        ));
3196    }
3197
3198    async fn log_stop_client_connections_request(
3199        &mut self,
3200        enabled_duration: zx::MonotonicDuration,
3201    ) {
3202        self.throttled_error_logger.throttle_error(log_cobalt!(
3203            self.cobalt_proxy,
3204            log_integer,
3205            metrics::CLIENT_CONNECTIONS_ENABLED_DURATION_MIGRATED_METRIC_ID,
3206            enabled_duration.into_micros(),
3207            &[],
3208        ));
3209    }
3210
3211    async fn log_stop_ap_cobalt_metrics(&mut self, enabled_duration: zx::MonotonicDuration) {
3212        self.throttled_error_logger.throttle_error(log_cobalt!(
3213            self.cobalt_proxy,
3214            log_integer,
3215            metrics::ACCESS_POINT_ENABLED_DURATION_MIGRATED_METRIC_ID,
3216            enabled_duration.into_micros(),
3217            &[],
3218        ));
3219    }
3220
3221    async fn log_signal_report_metrics(&mut self, rssi: i8, _tx_rate_500kbps: Option<u32>) {
3222        // The range of the RSSI histogram is -128 to 0 with bucket size 1. The buckets are:
3223        //     bucket 0: reserved for underflow, although not possible with i8
3224        //     bucket 1: -128
3225        //     bucket 2: -127
3226        //     ...
3227        //     bucket 129: 0
3228        //     bucket 130: overflow (1 and above)
3229        let index = min(130, rssi as i16 + 129) as u32;
3230        let entry = self
3231            .rssi_hist
3232            .entry(index)
3233            .or_insert(fidl_fuchsia_metrics::HistogramBucket { index, count: 0 });
3234        entry.count += 1;
3235
3236        // TODO(b/495495294): Create a Cobalt metric ID for negotiated data rate and use it to log `tx_rate_500kbps`.
3237    }
3238
3239    async fn log_signal_velocity_metrics(&mut self, rssi_velocity: f64) {
3240        // Add the count to the RSSI velocity histogram, which will be periodically logged.
3241        // The histogram range is -10 to 10, and index 0 is reserved for values below -10. For
3242        // example, RSSI velocity -10 should map to index 1 and velocity 0 should map to index 11.
3243        const RSSI_VELOCITY_MIN_IDX: f64 = 0.0;
3244        const RSSI_VELOCITY_MAX_IDX: f64 = 22.0;
3245        const RSSI_VELOCITY_HIST_OFFSET: f64 = 11.0;
3246        let index = (rssi_velocity + RSSI_VELOCITY_HIST_OFFSET)
3247            .clamp(RSSI_VELOCITY_MIN_IDX, RSSI_VELOCITY_MAX_IDX) as u32;
3248        let entry = self
3249            .rssi_velocity_hist
3250            .entry(index)
3251            .or_insert(fidl_fuchsia_metrics::HistogramBucket { index, count: 0 });
3252        entry.count += 1;
3253    }
3254
3255    async fn log_iface_creation_result(&mut self, result: Result<(), ()>) {
3256        if let Some(reason) = self.recovery_record.create_iface_failure.take() {
3257            match result {
3258                Ok(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Success).await,
3259                Err(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Failure).await,
3260            }
3261        }
3262    }
3263
3264    async fn log_iface_destruction_result(&mut self, result: Result<(), ()>) {
3265        if let Some(reason) = self.recovery_record.destroy_iface_failure.take() {
3266            match result {
3267                Ok(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Success).await,
3268                Err(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Failure).await,
3269            }
3270        }
3271    }
3272
3273    async fn log_scan_issues(&mut self, issues: Vec<ScanIssue>) {
3274        // If this is a scan result following a recovery intervention, judge whether or not the
3275        // recovery mechanism was successful.
3276        if let Some(reason) = self.recovery_record.scan_failure.take() {
3277            let outcome = match issues.contains(&ScanIssue::ScanFailure) {
3278                true => RecoveryOutcome::Failure,
3279                false => RecoveryOutcome::Success,
3280            };
3281            self.log_post_recovery_result(reason, outcome).await;
3282        }
3283        if let Some(reason) = self.recovery_record.scan_cancellation.take() {
3284            let outcome = match issues.contains(&ScanIssue::AbortedScan) {
3285                true => RecoveryOutcome::Failure,
3286                false => RecoveryOutcome::Success,
3287            };
3288            self.log_post_recovery_result(reason, outcome).await;
3289        }
3290        if let Some(reason) = self.recovery_record.scan_results_empty.take() {
3291            let outcome = match issues.contains(&ScanIssue::EmptyScanResults) {
3292                true => RecoveryOutcome::Failure,
3293                false => RecoveryOutcome::Success,
3294            };
3295            self.log_post_recovery_result(reason, outcome).await;
3296        }
3297
3298        // Log general occurrence metrics for any observed defects
3299        for issue in issues {
3300            self.throttled_error_logger.throttle_error(log_cobalt!(
3301                self.cobalt_proxy,
3302                log_occurrence,
3303                issue.as_metric_id(),
3304                1,
3305                &[]
3306            ))
3307        }
3308    }
3309
3310    async fn log_connection_failure(&mut self) {
3311        self.throttled_error_logger.throttle_error(log_cobalt!(
3312            self.cobalt_proxy,
3313            log_occurrence,
3314            metrics::CONNECTION_FAILURES_METRIC_ID,
3315            1,
3316            &[]
3317        ))
3318    }
3319
3320    async fn log_ap_start_result(&mut self, result: Result<(), ()>) {
3321        if result.is_err() {
3322            self.throttled_error_logger.throttle_error(log_cobalt!(
3323                self.cobalt_proxy,
3324                log_occurrence,
3325                metrics::AP_START_FAILURE_METRIC_ID,
3326                1,
3327                &[]
3328            ))
3329        }
3330
3331        if let Some(reason) = self.recovery_record.start_ap_failure.take() {
3332            match result {
3333                Ok(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Success).await,
3334                Err(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Failure).await,
3335            }
3336        }
3337    }
3338
3339    async fn log_scan_request_fulfillment_time(
3340        &mut self,
3341        duration: zx::MonotonicDuration,
3342        reason: client::scan::ScanReason,
3343    ) {
3344        let fulfillment_time_dim = {
3345            use metrics::ConnectivityWlanMetricDimensionScanFulfillmentTime::*;
3346            match duration.into_millis() {
3347                ..=0_000 => Unknown,
3348                1..=1_000 => LessThanOneSecond,
3349                1_001..=2_000 => LessThanTwoSeconds,
3350                2_001..=3_000 => LessThanThreeSeconds,
3351                3_001..=5_000 => LessThanFiveSeconds,
3352                5_001..=8_000 => LessThanEightSeconds,
3353                8_001..=13_000 => LessThanThirteenSeconds,
3354                13_001..=21_000 => LessThanTwentyOneSeconds,
3355                21_001..=34_000 => LessThanThirtyFourSeconds,
3356                34_001..=55_000 => LessThanFiftyFiveSeconds,
3357                55_001.. => MoreThanFiftyFiveSeconds,
3358            }
3359        };
3360        let reason_dim = {
3361            use client::scan::ScanReason;
3362            use metrics::ConnectivityWlanMetricDimensionScanReason::*;
3363            match reason {
3364                ScanReason::ClientRequest => ClientRequest,
3365                ScanReason::NetworkSelection => NetworkSelection,
3366                ScanReason::BssSelection => BssSelection,
3367                ScanReason::BssSelectionAugmentation => BssSelectionAugmentation,
3368                ScanReason::RoamSearch => ProactiveRoaming,
3369            }
3370        };
3371        self.throttled_error_logger.throttle_error(log_cobalt!(
3372            self.cobalt_proxy,
3373            log_occurrence,
3374            metrics::SUCCESSFUL_SCAN_REQUEST_FULFILLMENT_TIME_METRIC_ID,
3375            1,
3376            &[fulfillment_time_dim as u32, reason_dim as u32],
3377        ))
3378    }
3379
3380    async fn log_scan_queue_statistics(
3381        &mut self,
3382        fulfilled_requests: usize,
3383        remaining_requests: usize,
3384    ) {
3385        let fulfilled_requests_dim = {
3386            use metrics::ConnectivityWlanMetricDimensionScanRequestsFulfilled::*;
3387            match fulfilled_requests {
3388                0 => Zero,
3389                1 => One,
3390                2 => Two,
3391                3 => Three,
3392                4 => Four,
3393                5..=9 => FiveToNine,
3394                10.. => TenOrMore,
3395            }
3396        };
3397        let remaining_requests_dim = {
3398            use metrics::ConnectivityWlanMetricDimensionScanRequestsRemaining::*;
3399            match remaining_requests {
3400                0 => Zero,
3401                1 => One,
3402                2 => Two,
3403                3 => Three,
3404                4 => Four,
3405                5..=9 => FiveToNine,
3406                10..=14 => TenToFourteen,
3407                15.. => FifteenOrMore,
3408            }
3409        };
3410        self.throttled_error_logger.throttle_error(log_cobalt!(
3411            self.cobalt_proxy,
3412            log_occurrence,
3413            metrics::SCAN_QUEUE_STATISTICS_AFTER_COMPLETED_SCAN_METRIC_ID,
3414            1,
3415            &[fulfilled_requests_dim as u32, remaining_requests_dim as u32],
3416        ))
3417    }
3418
3419    async fn log_consecutive_counter_stats_failures(&mut self, count: i64) {
3420        self.throttled_error_logger.throttle_error(log_cobalt!(
3421            self.cobalt_proxy,
3422            log_integer,
3423            // TODO(https://fxbug.dev/404889275): Consider renaming the Cobalt
3424            // metric name to no longer to refer to "counter"
3425            metrics::CONSECUTIVE_COUNTER_STATS_FAILURES_METRIC_ID,
3426            count,
3427            &[]
3428        ))
3429    }
3430
3431    // Loops over the list of signal measurements, calculating what the RSSI exponentially-weighted
3432    // moving average and velocity were at that period in time. Then calculates an average "score"
3433    // over the entire list based on the EWMA RSSIs and velocities. Logs the average "score" - the
3434    // "score" of the baseline signal, with the time dimension event_code.
3435    //
3436    // This function is used to log 1) the delta between score at connect time and score over a
3437    // duration of time after, and 2) the delta between score at disconnect time and score over a
3438    // duration of time before.
3439    async fn log_average_delta_metric_by_signal(
3440        &mut self,
3441        metric_id: u32,
3442        signals: Vec<client::types::TimestampedSignal>,
3443        baseline_signal: client::types::Signal,
3444        time_dimension: u32,
3445    ) {
3446        if signals.is_empty() {
3447            warn!("Signals list for time dimension {:?} is empty.", time_dimension);
3448            return;
3449        }
3450        // Calculate the baseline score from the baseline signal.
3451        let mut ewma_signal = EwmaSignalData::new(
3452            baseline_signal.rssi_dbm,
3453            baseline_signal.snr_db,
3454            EWMA_SMOOTHING_FACTOR_FOR_METRICS,
3455        );
3456        let mut velocity = RssiVelocity::new(baseline_signal.rssi_dbm);
3457        let baseline_score =
3458            client::connection_selection::scoring_functions::score_current_connection_signal_data(
3459                ewma_signal,
3460                0.0,
3461            );
3462        let score_dimension = {
3463            // This dimension is the same for post-connect and pre-disconnect, representing the
3464            // first and last recorded score, respectively.
3465            use metrics::AverageScoreDeltaAfterConnectionByInitialScoreMetricDimensionInitialScore::*;
3466            match baseline_score {
3467                u8::MIN..=20 => _0To20,
3468                21..=40 => _21To40,
3469                41..=60 => _41To60,
3470                61..=80 => _61To80,
3471                81..=u8::MAX => _81To100,
3472            }
3473        };
3474        let mut sum_score = baseline_score as u32;
3475
3476        // For each entry, update the ewma signal and velocity and calculate the score, using
3477        // saturating arithmetic to ensure overflow panics are impossible. In practice, integers for
3478        // this metric should not be remotely near overflowing.
3479        for timed_signal in &signals {
3480            ewma_signal.update_with_new_measurement(
3481                timed_signal.signal.rssi_dbm,
3482                timed_signal.signal.snr_db,
3483            );
3484            velocity.update(ewma_signal.ewma_rssi.get());
3485            let score = client::connection_selection::scoring_functions::score_current_connection_signal_data(ewma_signal, velocity.get());
3486            sum_score = sum_score.saturating_add(score as u32);
3487        }
3488
3489        // Calculate the average score over the recorded time frame.
3490        let avg_score = sum_score / (signals.len() + 1) as u32;
3491
3492        let delta = (avg_score as i64).saturating_sub(baseline_score as i64);
3493        self.throttled_error_logger.throttle_error(log_cobalt!(
3494            &self.cobalt_proxy,
3495            log_integer,
3496            metric_id,
3497            delta,
3498            &[score_dimension as u32, time_dimension],
3499        ));
3500    }
3501
3502    // Loops over the list of signal measurements, calculating the average RSSI. Logs the average
3503    // RSSI - the RSSI of the baseline signal, with the time dimension event_code.
3504    //
3505    // This function is used to log 1) the delta between RSSI at connect time and RSSI over a
3506    // duration of time after, and 2) the delta between RSSI at disconnect time and RSSI over a
3507    // duration of time before.
3508    async fn log_average_rssi_delta_metric(
3509        &mut self,
3510        metric_id: u32,
3511        signals: Vec<client::types::TimestampedSignal>,
3512        baseline_signal: client::types::Signal,
3513        time_dimension: u32,
3514    ) {
3515        if signals.is_empty() {
3516            warn!("Signals list for time dimension {:?} is empty.", time_dimension);
3517            return;
3518        }
3519
3520        let rssi_dimension = {
3521            use metrics::AverageRssiDeltaAfterConnectionByInitialRssiMetricDimensionRssiBucket::*;
3522            match baseline_signal.rssi_dbm {
3523                i8::MIN..=-90 => From128To90,
3524                -89..=-86 => From89To86,
3525                -85..=-83 => From85To83,
3526                -82..=-80 => From82To80,
3527                -79..=-77 => From79To77,
3528                -76..=-74 => From76To74,
3529                -73..=-71 => From73To71,
3530                -70..=-66 => From70To66,
3531                -65..=-61 => From65To61,
3532                -60..=-51 => From60To51,
3533                -50..=-35 => From50To35,
3534                -34..=-28 => From34To28,
3535                -27..=-1 => From27To1,
3536                0..=i8::MAX => _0,
3537            }
3538        };
3539        // Calculate the average RSSI over the recorded time frame.
3540        let mut sum_rssi = baseline_signal.rssi_dbm as i64;
3541        for s in &signals {
3542            sum_rssi = sum_rssi.saturating_add(s.signal.rssi_dbm as i64);
3543        }
3544        let average_rssi = sum_rssi / (signals.len() + 1) as i64;
3545
3546        let delta = (average_rssi).saturating_sub(baseline_signal.rssi_dbm as i64);
3547        self.throttled_error_logger.throttle_error(log_cobalt!(
3548            &self.cobalt_proxy,
3549            log_integer,
3550            metric_id,
3551            delta,
3552            &[rssi_dimension as u32, time_dimension],
3553        ));
3554    }
3555
3556    async fn log_post_connection_score_deltas_by_signal<const N: usize>(
3557        &mut self,
3558        connect_time: fasync::MonotonicInstant,
3559        signal_at_connect: client::types::Signal,
3560        signals: HistoricalList<client::types::TimestampedSignal, N>,
3561    ) {
3562        // The following time ranges are 100ms longer than the corresponding duration dimensions.
3563        // Scores should be logged every 1 second, but the extra time provides a buffer reports are
3564        // not perfectly periodic.
3565        use metrics::AverageScoreDeltaAfterConnectionByInitialScoreMetricDimensionTimeSinceConnect as DurationDimension;
3566
3567        self.log_average_delta_metric_by_signal(
3568            metrics::AVERAGE_SCORE_DELTA_AFTER_CONNECTION_BY_INITIAL_SCORE_METRIC_ID,
3569            signals
3570                .get_between(connect_time, connect_time + zx::MonotonicDuration::from_millis(1100)),
3571            signal_at_connect,
3572            DurationDimension::OneSecond as u32,
3573        )
3574        .await;
3575
3576        self.log_average_delta_metric_by_signal(
3577            metrics::AVERAGE_SCORE_DELTA_AFTER_CONNECTION_BY_INITIAL_SCORE_METRIC_ID,
3578            signals
3579                .get_between(connect_time, connect_time + zx::MonotonicDuration::from_millis(5100)),
3580            signal_at_connect,
3581            DurationDimension::FiveSeconds as u32,
3582        )
3583        .await;
3584
3585        self.log_average_delta_metric_by_signal(
3586            metrics::AVERAGE_SCORE_DELTA_AFTER_CONNECTION_BY_INITIAL_SCORE_METRIC_ID,
3587            signals.get_between(
3588                connect_time,
3589                connect_time + zx::MonotonicDuration::from_millis(10100),
3590            ),
3591            signal_at_connect,
3592            DurationDimension::TenSeconds as u32,
3593        )
3594        .await;
3595
3596        self.log_average_delta_metric_by_signal(
3597            metrics::AVERAGE_SCORE_DELTA_AFTER_CONNECTION_BY_INITIAL_SCORE_METRIC_ID,
3598            signals.get_between(
3599                connect_time,
3600                connect_time + zx::MonotonicDuration::from_millis(30100),
3601            ),
3602            signal_at_connect,
3603            DurationDimension::ThirtySeconds as u32,
3604        )
3605        .await;
3606    }
3607
3608    async fn log_pre_disconnect_score_deltas_by_signal<const N: usize>(
3609        &mut self,
3610        connect_duration: zx::MonotonicDuration,
3611        mut signals: HistoricalList<client::types::TimestampedSignal, N>,
3612    ) {
3613        // The following time ranges are 100ms longer than the corresponding duration dimensions.
3614        // Scores should be logged every 1 second, but the extra time provides a buffer reports are
3615        // not perfectly periodic.
3616        use metrics::AverageScoreDeltaBeforeDisconnectByFinalScoreMetricDimensionTimeUntilDisconnect as DurationDimension;
3617        if connect_duration >= AVERAGE_SCORE_DELTA_MINIMUM_DURATION {
3618            // Get the last recorded score before the disconnect occurs.
3619            if let Some(client::types::TimestampedSignal {
3620                signal: final_signal,
3621                time: final_signal_time,
3622            }) = signals.0.pop_back()
3623            {
3624                self.log_average_delta_metric_by_signal(
3625                    metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
3626                    signals
3627                        .get_recent(final_signal_time - zx::MonotonicDuration::from_millis(1100)),
3628                    final_signal,
3629                    DurationDimension::OneSecond as u32,
3630                )
3631                .await;
3632                self.log_average_delta_metric_by_signal(
3633                    metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
3634                    signals
3635                        .get_recent(final_signal_time - zx::MonotonicDuration::from_millis(5100)),
3636                    final_signal,
3637                    DurationDimension::FiveSeconds as u32,
3638                )
3639                .await;
3640                self.log_average_delta_metric_by_signal(
3641                    metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
3642                    signals
3643                        .get_recent(final_signal_time - zx::MonotonicDuration::from_millis(10100)),
3644                    final_signal,
3645                    DurationDimension::TenSeconds as u32,
3646                )
3647                .await;
3648                self.log_average_delta_metric_by_signal(
3649                    metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
3650                    signals
3651                        .get_recent(final_signal_time - zx::MonotonicDuration::from_millis(30100)),
3652                    final_signal,
3653                    DurationDimension::ThirtySeconds as u32,
3654                )
3655                .await;
3656            } else {
3657                warn!("Past signals list is unexpectedly empty");
3658            }
3659        }
3660    }
3661
3662    async fn log_post_connection_rssi_deltas<const N: usize>(
3663        &mut self,
3664        connect_time: fasync::MonotonicInstant,
3665        signal_at_connect: client::types::Signal,
3666        signals: HistoricalList<client::types::TimestampedSignal, N>,
3667    ) {
3668        // The following time ranges are 100ms longer than the corresponding duration dimensions.
3669        // RSSI should be logged every 1 second, but the extra time provides a buffer reports are
3670        // not perfectly periodic.
3671        use metrics::AverageRssiDeltaAfterConnectionByInitialRssiMetricDimensionTimeSinceConnect as DurationDimension;
3672
3673        self.log_average_rssi_delta_metric(
3674            metrics::AVERAGE_RSSI_DELTA_AFTER_CONNECTION_BY_INITIAL_RSSI_METRIC_ID,
3675            signals
3676                .get_between(connect_time, connect_time + zx::MonotonicDuration::from_millis(1100)),
3677            signal_at_connect,
3678            DurationDimension::OneSecond as u32,
3679        )
3680        .await;
3681
3682        self.log_average_rssi_delta_metric(
3683            metrics::AVERAGE_RSSI_DELTA_AFTER_CONNECTION_BY_INITIAL_RSSI_METRIC_ID,
3684            signals
3685                .get_between(connect_time, connect_time + zx::MonotonicDuration::from_millis(5100)),
3686            signal_at_connect,
3687            DurationDimension::FiveSeconds as u32,
3688        )
3689        .await;
3690
3691        self.log_average_rssi_delta_metric(
3692            metrics::AVERAGE_RSSI_DELTA_AFTER_CONNECTION_BY_INITIAL_RSSI_METRIC_ID,
3693            signals.get_between(
3694                connect_time,
3695                connect_time + zx::MonotonicDuration::from_millis(10100),
3696            ),
3697            signal_at_connect,
3698            DurationDimension::TenSeconds as u32,
3699        )
3700        .await;
3701
3702        self.log_average_rssi_delta_metric(
3703            metrics::AVERAGE_RSSI_DELTA_AFTER_CONNECTION_BY_INITIAL_RSSI_METRIC_ID,
3704            signals.get_between(
3705                connect_time,
3706                connect_time + zx::MonotonicDuration::from_millis(30100),
3707            ),
3708            signal_at_connect,
3709            DurationDimension::ThirtySeconds as u32,
3710        )
3711        .await;
3712    }
3713
3714    async fn log_pre_disconnect_rssi_deltas<const N: usize>(
3715        &mut self,
3716        connect_duration: zx::MonotonicDuration,
3717        mut signals: HistoricalList<client::types::TimestampedSignal, N>,
3718    ) {
3719        // The following time ranges are 100ms longer than the corresponding duration dimensions.
3720        // RSSI should be logged every 1 second, but the extra time provides a buffer reports are
3721        // not perfectly periodic.
3722        use metrics::AverageRssiDeltaAfterConnectionByInitialRssiMetricDimensionTimeSinceConnect as DurationDimension;
3723
3724        if connect_duration >= AVERAGE_SCORE_DELTA_MINIMUM_DURATION {
3725            // Get the last recorded score before the disconnect occurs.
3726            if let Some(client::types::TimestampedSignal {
3727                signal: final_signal,
3728                time: final_signal_time,
3729            }) = signals.0.pop_back()
3730            {
3731                self.log_average_rssi_delta_metric(
3732                    metrics::AVERAGE_RSSI_DELTA_BEFORE_DISCONNECT_BY_FINAL_RSSI_METRIC_ID,
3733                    signals.get_between(
3734                        final_signal_time - zx::MonotonicDuration::from_millis(1100),
3735                        final_signal_time,
3736                    ),
3737                    final_signal,
3738                    DurationDimension::OneSecond as u32,
3739                )
3740                .await;
3741
3742                self.log_average_rssi_delta_metric(
3743                    metrics::AVERAGE_RSSI_DELTA_BEFORE_DISCONNECT_BY_FINAL_RSSI_METRIC_ID,
3744                    signals.get_between(
3745                        final_signal_time - zx::MonotonicDuration::from_millis(5100),
3746                        final_signal_time,
3747                    ),
3748                    final_signal,
3749                    DurationDimension::FiveSeconds as u32,
3750                )
3751                .await;
3752
3753                self.log_average_rssi_delta_metric(
3754                    metrics::AVERAGE_RSSI_DELTA_BEFORE_DISCONNECT_BY_FINAL_RSSI_METRIC_ID,
3755                    signals.get_between(
3756                        final_signal_time - zx::MonotonicDuration::from_millis(10100),
3757                        final_signal_time,
3758                    ),
3759                    final_signal,
3760                    DurationDimension::TenSeconds as u32,
3761                )
3762                .await;
3763
3764                self.log_average_rssi_delta_metric(
3765                    metrics::AVERAGE_RSSI_DELTA_BEFORE_DISCONNECT_BY_FINAL_RSSI_METRIC_ID,
3766                    signals.get_between(
3767                        final_signal_time - zx::MonotonicDuration::from_millis(30100),
3768                        final_signal_time,
3769                    ),
3770                    final_signal,
3771                    DurationDimension::ThirtySeconds as u32,
3772                )
3773                .await;
3774            }
3775        }
3776    }
3777
3778    async fn log_short_duration_connection_metrics<const N: usize>(
3779        &mut self,
3780        signals: HistoricalList<client::types::TimestampedSignal, N>,
3781        disconnect_source: fidl_sme::DisconnectSource,
3782        previous_connect_reason: client::types::ConnectReason,
3783    ) {
3784        self.log_connection_score_average_by_signal(
3785            metrics::ConnectionScoreAverageMetricDimensionDuration::ShortDuration as u32,
3786            signals.get_before(fasync::MonotonicInstant::now()),
3787        )
3788        .await;
3789        self.log_connection_rssi_average(
3790            metrics::ConnectionRssiAverageMetricDimensionDuration::ShortDuration as u32,
3791            signals.get_before(fasync::MonotonicInstant::now()),
3792        )
3793        .await;
3794        // Logs user requested connection during short duration connection, which indicates that we
3795        // did not successfully select the user's preferred connection.
3796        match disconnect_source {
3797            fidl_sme::DisconnectSource::User(
3798                fidl_sme::UserDisconnectReason::FidlConnectRequest,
3799            )
3800            | fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::NetworkUnsaved) => {
3801                let metric_events = vec![
3802                    MetricEvent {
3803                        metric_id: metrics::POLICY_FIDL_CONNECTION_ATTEMPTS_DURING_SHORT_CONNECTION_METRIC_ID,
3804                        event_codes: vec![],
3805                        payload: MetricEventPayload::Count(1),
3806                    },
3807                    MetricEvent {
3808                        metric_id: metrics::POLICY_FIDL_CONNECTION_ATTEMPTS_DURING_SHORT_CONNECTION_DETAILED_METRIC_ID,
3809                        event_codes: vec![previous_connect_reason as u32],
3810                        payload: MetricEventPayload::Count(1),
3811                    }
3812                ];
3813
3814                self.throttled_error_logger.throttle_error(log_cobalt_batch!(
3815                    self.cobalt_proxy,
3816                    &metric_events,
3817                    "log_short_duration_connection_metrics",
3818                ));
3819            }
3820            _ => {}
3821        }
3822    }
3823
3824    async fn log_network_selection_metrics(
3825        &mut self,
3826        connection_state: &mut ConnectionState,
3827        network_selection_type: NetworkSelectionType,
3828        num_candidates: Result<usize, ()>,
3829        selected_count: usize,
3830    ) {
3831        let now = fasync::MonotonicInstant::now();
3832        let mut metric_events = vec![];
3833        metric_events.push(MetricEvent {
3834            metric_id: metrics::NETWORK_SELECTION_COUNT_METRIC_ID,
3835            event_codes: vec![],
3836            payload: MetricEventPayload::Count(1),
3837        });
3838
3839        match num_candidates {
3840            Ok(n) if n > 0 => {
3841                // Saved neighbors are seen, so clear the "no saved neighbor" flag. Account
3842                // for any untracked time to the `downtime_no_saved_neighbor_duration`
3843                // counter.
3844                if let ConnectionState::Disconnected(state) = connection_state
3845                    && let Some(prev) = state.latest_no_saved_neighbor_time.take()
3846                {
3847                    let duration = now - prev;
3848                    state.accounted_no_saved_neighbor_duration += duration;
3849                    self.queue_stat_op(StatOp::AddDowntimeNoSavedNeighborDuration(duration));
3850                }
3851
3852                if network_selection_type == NetworkSelectionType::Undirected {
3853                    // Log number of selected networks if a network was not specified.
3854                    metric_events.push(MetricEvent {
3855                        metric_id: metrics::NUM_NETWORKS_SELECTED_METRIC_ID,
3856                        event_codes: vec![],
3857                        payload: MetricEventPayload::IntegerValue(selected_count as i64),
3858                    });
3859                }
3860            }
3861            Ok(0) if network_selection_type == NetworkSelectionType::Undirected => {
3862                // No saved neighbor is seen. If "no saved neighbor" flag isn't set, then
3863                // set it to the current time. Otherwise, do nothing because the telemetry
3864                // loop will account for untracked downtime during periodic telemetry run.
3865                if let ConnectionState::Disconnected(state) = connection_state
3866                    && state.latest_no_saved_neighbor_time.is_none()
3867                {
3868                    state.latest_no_saved_neighbor_time = Some(now);
3869                }
3870            }
3871            _ => (),
3872        }
3873
3874        self.throttled_error_logger.throttle_error(log_cobalt_batch!(
3875            self.cobalt_proxy,
3876            &metric_events,
3877            "log_network_selection_metrics",
3878        ));
3879    }
3880
3881    #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
3882    async fn log_bss_selection_metrics(
3883        &mut self,
3884        reason: client::types::ConnectReason,
3885        mut scored_candidates: Vec<(client::types::ScannedCandidate, i16)>,
3886        selected_candidate: Option<(client::types::ScannedCandidate, i16)>,
3887    ) {
3888        let mut metric_events = vec![];
3889
3890        // Record dimensionless BSS selection count
3891        metric_events.push(MetricEvent {
3892            metric_id: metrics::BSS_SELECTION_COUNT_METRIC_ID,
3893            event_codes: vec![],
3894            payload: MetricEventPayload::Count(1),
3895        });
3896
3897        // Record detailed BSS selection count
3898        metric_events.push(MetricEvent {
3899            metric_id: metrics::BSS_SELECTION_COUNT_DETAILED_METRIC_ID,
3900            event_codes: vec![reason as u32],
3901            payload: MetricEventPayload::Count(1),
3902        });
3903
3904        // Record dimensionless number of BSS candidates
3905        metric_events.push(MetricEvent {
3906            metric_id: metrics::NUM_BSS_CONSIDERED_IN_SELECTION_METRIC_ID,
3907            event_codes: vec![],
3908            payload: MetricEventPayload::IntegerValue(scored_candidates.len() as i64),
3909        });
3910        // Record detailed number of BSS candidates
3911        metric_events.push(MetricEvent {
3912            metric_id: metrics::NUM_BSS_CONSIDERED_IN_SELECTION_DETAILED_METRIC_ID,
3913            event_codes: vec![reason as u32],
3914            payload: MetricEventPayload::IntegerValue(scored_candidates.len() as i64),
3915        });
3916
3917        if !scored_candidates.is_empty() {
3918            let (mut best_score_2g, mut best_score_5g) = (None, None);
3919            let mut unique_networks = HashSet::new();
3920
3921            for (candidate, score) in &scored_candidates {
3922                // Record candidate's score
3923                metric_events.push(MetricEvent {
3924                    metric_id: metrics::BSS_CANDIDATE_SCORE_METRIC_ID,
3925                    event_codes: vec![],
3926                    payload: MetricEventPayload::IntegerValue(*score as i64),
3927                });
3928
3929                let _ = unique_networks.insert(&candidate.network);
3930
3931                if candidate.bss.channel.band == fidl_ieee80211::WlanBand::TwoGhz {
3932                    best_score_2g = best_score_2g.or(Some(*score)).map(|s| max(s, *score));
3933                } else {
3934                    best_score_5g = best_score_5g.or(Some(*score)).map(|s| max(s, *score));
3935                }
3936            }
3937
3938            // Record number of unique networks in bss selection. This differs from number of
3939            // networks selected, since some actions may bypass network selection (e.g. proactive
3940            // roaming)
3941            metric_events.push(MetricEvent {
3942                metric_id: metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID,
3943                event_codes: vec![reason as u32],
3944                payload: MetricEventPayload::IntegerValue(unique_networks.len() as i64),
3945            });
3946
3947            if let Some((_, score)) = selected_candidate {
3948                // Record selected candidate's score
3949                metric_events.push(MetricEvent {
3950                    metric_id: metrics::SELECTED_BSS_SCORE_METRIC_ID,
3951                    event_codes: vec![],
3952                    payload: MetricEventPayload::IntegerValue(score as i64),
3953                });
3954
3955                // Record runner-up candidate's score, iff:
3956                // 1. there were multiple candidates and
3957                // 2. selected candidate is the top scoring candidate (or tied in score)
3958                scored_candidates.sort_by_key(|(_, score)| Reverse(*score));
3959                #[expect(clippy::get_first)]
3960                if let (Some(first_candidate), Some(second_candidate)) =
3961                    (scored_candidates.get(0), scored_candidates.get(1))
3962                    && score == first_candidate.1
3963                {
3964                    let delta = first_candidate.1 - second_candidate.1;
3965                    metric_events.push(MetricEvent {
3966                        metric_id: metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID,
3967                        event_codes: vec![],
3968                        payload: MetricEventPayload::IntegerValue(delta as i64),
3969                    });
3970                }
3971            }
3972
3973            let ghz_event_code =
3974                if let (Some(score_2g), Some(score_5g)) = (best_score_2g, best_score_5g) {
3975                    // Record delta between best 5GHz and best 2.4GHz candidates
3976                    metric_events.push(MetricEvent {
3977                        metric_id: metrics::BEST_CANDIDATES_GHZ_SCORE_DELTA_METRIC_ID,
3978                        event_codes: vec![],
3979                        payload: MetricEventPayload::IntegerValue((score_5g - score_2g) as i64),
3980                    });
3981                    metrics::ConnectivityWlanMetricDimensionBands::MultiBand
3982                } else if best_score_2g.is_some() {
3983                    metrics::ConnectivityWlanMetricDimensionBands::Band2Dot4Ghz
3984                } else {
3985                    metrics::ConnectivityWlanMetricDimensionBands::Band5Ghz
3986                };
3987
3988            metric_events.push(MetricEvent {
3989                metric_id: metrics::GHZ_BANDS_AVAILABLE_IN_BSS_SELECTION_METRIC_ID,
3990                event_codes: vec![ghz_event_code as u32],
3991                payload: MetricEventPayload::Count(1),
3992            });
3993        }
3994
3995        self.throttled_error_logger.throttle_error(log_cobalt_batch!(
3996            self.cobalt_proxy,
3997            &metric_events,
3998            "log_bss_selection_cobalt_metrics",
3999        ));
4000    }
4001
4002    async fn log_connection_score_average_by_signal(
4003        &mut self,
4004        duration_dim: u32,
4005        signals: Vec<client::types::TimestampedSignal>,
4006    ) {
4007        let Some(first_signal) = signals.first() else {
4008            warn!("Connection signals list is unexpectedly empty.");
4009            return;
4010        };
4011        let mut sum_scores = 0;
4012        let mut ewma_signal = EwmaSignalData::new(
4013            first_signal.signal.rssi_dbm,
4014            first_signal.signal.snr_db,
4015            EWMA_SMOOTHING_FACTOR_FOR_METRICS,
4016        );
4017        let mut velocity = RssiVelocity::new(first_signal.signal.rssi_dbm);
4018        for timed_signal in &signals {
4019            ewma_signal.update_with_new_measurement(
4020                timed_signal.signal.rssi_dbm,
4021                timed_signal.signal.snr_db,
4022            );
4023            velocity.update(ewma_signal.ewma_rssi.get());
4024            let score = client::connection_selection::scoring_functions::score_current_connection_signal_data(ewma_signal, velocity.get());
4025            sum_scores = sum_scores.saturating_add(&(score as u32));
4026        }
4027        let avg = sum_scores / (signals.len()) as u32;
4028        self.throttled_error_logger.throttle_error(log_cobalt!(
4029            self.cobalt_proxy,
4030            log_integer,
4031            metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID,
4032            avg as i64,
4033            &[duration_dim],
4034        ));
4035    }
4036
4037    async fn log_connection_rssi_average(
4038        &mut self,
4039        duration_dim: u32,
4040        signals: Vec<client::types::TimestampedSignal>,
4041    ) {
4042        if signals.is_empty() {
4043            warn!("Connection signals list is unexpectedly empty.");
4044            return;
4045        }
4046        let mut sum_rssi: i64 = 0;
4047        for s in &signals {
4048            sum_rssi = sum_rssi.saturating_add(s.signal.rssi_dbm as i64);
4049        }
4050        let average_rssi = sum_rssi / (signals.len()) as i64;
4051        self.throttled_error_logger.throttle_error(log_cobalt!(
4052            self.cobalt_proxy,
4053            log_integer,
4054            metrics::CONNECTION_RSSI_AVERAGE_METRIC_ID,
4055            average_rssi,
4056            &[duration_dim]
4057        ));
4058    }
4059
4060    async fn log_recovery_occurrence(&mut self, reason: RecoveryReason) {
4061        self.recovery_record.record_recovery_attempt(reason);
4062
4063        let dimension = match reason {
4064            RecoveryReason::CreateIfaceFailure(_) => {
4065                metrics::RecoveryOccurrenceMetricDimensionReason::InterfaceCreationFailure
4066            }
4067            RecoveryReason::DestroyIfaceFailure(_) => {
4068                metrics::RecoveryOccurrenceMetricDimensionReason::InterfaceDestructionFailure
4069            }
4070            RecoveryReason::Timeout(_) => metrics::RecoveryOccurrenceMetricDimensionReason::Timeout,
4071            RecoveryReason::ConnectFailure(_) => {
4072                metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure
4073            }
4074            RecoveryReason::StartApFailure(_) => {
4075                metrics::RecoveryOccurrenceMetricDimensionReason::ApStartFailure
4076            }
4077            RecoveryReason::ScanFailure(_) => {
4078                metrics::RecoveryOccurrenceMetricDimensionReason::ScanFailure
4079            }
4080            RecoveryReason::ScanCancellation(_) => {
4081                metrics::RecoveryOccurrenceMetricDimensionReason::ScanCancellation
4082            }
4083            RecoveryReason::ScanResultsEmpty(_) => {
4084                metrics::RecoveryOccurrenceMetricDimensionReason::ScanResultsEmpty
4085            }
4086        };
4087
4088        self.throttled_error_logger.throttle_error(log_cobalt!(
4089            self.cobalt_proxy,
4090            log_occurrence,
4091            metrics::RECOVERY_OCCURRENCE_METRIC_ID,
4092            1,
4093            &[dimension.as_event_code()],
4094        ))
4095    }
4096
4097    async fn log_post_recovery_result(&mut self, reason: RecoveryReason, outcome: RecoveryOutcome) {
4098        async fn log_post_recovery_metric(
4099            throttled_error_logger: &mut ThrottledErrorLogger,
4100            proxy: &mut fidl_fuchsia_metrics::MetricEventLoggerProxy,
4101            metric_id: u32,
4102            event_codes: &[u32],
4103        ) {
4104            throttled_error_logger.throttle_error(log_cobalt!(
4105                proxy,
4106                log_occurrence,
4107                metric_id,
4108                1,
4109                event_codes,
4110            ))
4111        }
4112
4113        if outcome == RecoveryOutcome::Success {
4114            self.last_successful_recovery.set(fasync::MonotonicInstant::now().into_nanos() as u64);
4115            let _ = self.successful_recoveries.add(1);
4116        }
4117
4118        match reason {
4119            RecoveryReason::CreateIfaceFailure(_) => {
4120                log_post_recovery_metric(
4121                    &mut self.throttled_error_logger,
4122                    &mut self.cobalt_proxy,
4123                    metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
4124                    &[outcome.as_event_code()],
4125                )
4126                .await;
4127            }
4128            RecoveryReason::DestroyIfaceFailure(_) => {
4129                log_post_recovery_metric(
4130                    &mut self.throttled_error_logger,
4131                    &mut self.cobalt_proxy,
4132                    metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
4133                    &[outcome.as_event_code()],
4134                )
4135                .await;
4136            }
4137            RecoveryReason::Timeout(mechanism) => {
4138                log_post_recovery_metric(
4139                    &mut self.throttled_error_logger,
4140                    &mut self.cobalt_proxy,
4141                    metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
4142                    &[outcome.as_event_code(), mechanism.as_event_code()],
4143                )
4144                .await;
4145            }
4146            RecoveryReason::ConnectFailure(mechanism) => {
4147                log_post_recovery_metric(
4148                    &mut self.throttled_error_logger,
4149                    &mut self.cobalt_proxy,
4150                    metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
4151                    &[outcome.as_event_code(), mechanism.as_event_code()],
4152                )
4153                .await;
4154            }
4155            RecoveryReason::StartApFailure(mechanism) => {
4156                log_post_recovery_metric(
4157                    &mut self.throttled_error_logger,
4158                    &mut self.cobalt_proxy,
4159                    metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
4160                    &[outcome.as_event_code(), mechanism.as_event_code()],
4161                )
4162                .await;
4163            }
4164            RecoveryReason::ScanFailure(mechanism) => {
4165                log_post_recovery_metric(
4166                    &mut self.throttled_error_logger,
4167                    &mut self.cobalt_proxy,
4168                    metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
4169                    &[outcome.as_event_code(), mechanism.as_event_code()],
4170                )
4171                .await;
4172            }
4173            RecoveryReason::ScanCancellation(mechanism) => {
4174                log_post_recovery_metric(
4175                    &mut self.throttled_error_logger,
4176                    &mut self.cobalt_proxy,
4177                    metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
4178                    &[outcome.as_event_code(), mechanism.as_event_code()],
4179                )
4180                .await;
4181            }
4182            RecoveryReason::ScanResultsEmpty(mechanism) => {
4183                log_post_recovery_metric(
4184                    &mut self.throttled_error_logger,
4185                    &mut self.cobalt_proxy,
4186                    metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
4187                    &[outcome.as_event_code(), mechanism.as_event_code()],
4188                )
4189                .await;
4190            }
4191        }
4192    }
4193}
4194
4195#[allow(clippy::enum_variant_names, reason = "mass allow for https://fxbug.dev/381896734")]
4196enum StatOp {
4197    AddTotalDuration(zx::MonotonicDuration),
4198    AddConnectedDuration(zx::MonotonicDuration),
4199    AddDowntimeDuration(zx::MonotonicDuration),
4200    // Downtime with no saved network in vicinity
4201    AddDowntimeNoSavedNeighborDuration(zx::MonotonicDuration),
4202    AddConnectAttemptsCount,
4203    AddConnectSuccessfulCount,
4204    AddDisconnectCount(fidl_sme::DisconnectSource),
4205    AddPolicyRoamAttemptsCount(Vec<RoamReason>),
4206    AddPolicyRoamSuccessfulCount(Vec<RoamReason>),
4207    AddPolicyRoamDisconnectsCount,
4208    AddTxHighPacketDropDuration(zx::MonotonicDuration),
4209    AddRxHighPacketDropDuration(zx::MonotonicDuration),
4210    AddTxVeryHighPacketDropDuration(zx::MonotonicDuration),
4211    AddRxVeryHighPacketDropDuration(zx::MonotonicDuration),
4212    AddNoRxDuration(zx::MonotonicDuration),
4213}
4214
4215#[derive(Clone, PartialEq, Default)]
4216struct StatCounters {
4217    total_duration: zx::MonotonicDuration,
4218    connected_duration: zx::MonotonicDuration,
4219    downtime_duration: zx::MonotonicDuration,
4220    downtime_no_saved_neighbor_duration: zx::MonotonicDuration,
4221    connect_attempts_count: u64,
4222    connect_successful_count: u64,
4223    disconnect_count: u64,
4224    total_non_roam_disconnect_count: u64,
4225    total_roam_disconnect_count: u64,
4226    policy_roam_attempts_count: u64,
4227    policy_roam_successful_count: u64,
4228    policy_roam_disconnects_count: u64,
4229    policy_roam_attempts_count_by_roam_reason: HashMap<RoamReason, u64>,
4230    policy_roam_successful_count_by_roam_reason: HashMap<RoamReason, u64>,
4231    tx_high_packet_drop_duration: zx::MonotonicDuration,
4232    rx_high_packet_drop_duration: zx::MonotonicDuration,
4233    tx_very_high_packet_drop_duration: zx::MonotonicDuration,
4234    rx_very_high_packet_drop_duration: zx::MonotonicDuration,
4235    no_rx_duration: zx::MonotonicDuration,
4236}
4237
4238impl StatCounters {
4239    fn adjusted_downtime(&self) -> zx::MonotonicDuration {
4240        max(
4241            zx::MonotonicDuration::from_seconds(0),
4242            self.downtime_duration - self.downtime_no_saved_neighbor_duration,
4243        )
4244    }
4245
4246    fn connection_success_rate(&self) -> f64 {
4247        self.connect_successful_count as f64 / self.connect_attempts_count as f64
4248    }
4249
4250    fn policy_roam_success_rate(&self) -> f64 {
4251        self.policy_roam_successful_count as f64 / self.policy_roam_attempts_count as f64
4252    }
4253
4254    fn policy_roam_success_rate_by_roam_reason(&self, reason: &RoamReason) -> f64 {
4255        self.policy_roam_successful_count_by_roam_reason.get(reason).copied().unwrap_or(0) as f64
4256            / self.policy_roam_attempts_count_by_roam_reason.get(reason).copied().unwrap_or(0)
4257                as f64
4258    }
4259}
4260
4261// `Add` implementation is required to implement `SaturatingAdd` down below.
4262impl Add for StatCounters {
4263    type Output = Self;
4264
4265    fn add(self, other: Self) -> Self {
4266        // Merge the hashmap stats, summing duplicate entries.
4267        let mut policy_roam_attempts_count_by_roam_reason =
4268            other.policy_roam_attempts_count_by_roam_reason.clone();
4269        for (reason, count) in self.policy_roam_attempts_count_by_roam_reason {
4270            *policy_roam_attempts_count_by_roam_reason.entry(reason).or_insert(0) += count
4271        }
4272        let mut policy_roam_successful_count_by_roam_reason =
4273            other.policy_roam_successful_count_by_roam_reason.clone();
4274        for (reason, count) in self.policy_roam_successful_count_by_roam_reason {
4275            *policy_roam_successful_count_by_roam_reason.entry(reason).or_insert(0) += count
4276        }
4277
4278        Self {
4279            total_duration: self.total_duration + other.total_duration,
4280            connected_duration: self.connected_duration + other.connected_duration,
4281            downtime_duration: self.downtime_duration + other.downtime_duration,
4282            downtime_no_saved_neighbor_duration: self.downtime_no_saved_neighbor_duration
4283                + other.downtime_no_saved_neighbor_duration,
4284            connect_attempts_count: self.connect_attempts_count + other.connect_attempts_count,
4285            connect_successful_count: self.connect_successful_count
4286                + other.connect_successful_count,
4287            disconnect_count: self.disconnect_count + other.disconnect_count,
4288            total_non_roam_disconnect_count: self.total_non_roam_disconnect_count
4289                + other.total_non_roam_disconnect_count,
4290            total_roam_disconnect_count: self.total_roam_disconnect_count
4291                + other.total_roam_disconnect_count,
4292            policy_roam_attempts_count: self.policy_roam_attempts_count
4293                + other.policy_roam_attempts_count,
4294            policy_roam_successful_count: self.policy_roam_successful_count
4295                + other.policy_roam_successful_count,
4296            policy_roam_disconnects_count: self.policy_roam_disconnects_count
4297                + other.policy_roam_disconnects_count,
4298            policy_roam_attempts_count_by_roam_reason,
4299            policy_roam_successful_count_by_roam_reason,
4300            tx_high_packet_drop_duration: self.tx_high_packet_drop_duration
4301                + other.tx_high_packet_drop_duration,
4302            rx_high_packet_drop_duration: self.rx_high_packet_drop_duration
4303                + other.rx_high_packet_drop_duration,
4304            tx_very_high_packet_drop_duration: self.tx_very_high_packet_drop_duration
4305                + other.tx_very_high_packet_drop_duration,
4306            rx_very_high_packet_drop_duration: self.rx_very_high_packet_drop_duration
4307                + other.rx_very_high_packet_drop_duration,
4308            no_rx_duration: self.no_rx_duration + other.no_rx_duration,
4309        }
4310    }
4311}
4312
4313impl SaturatingAdd for StatCounters {
4314    fn saturating_add(&self, v: &Self) -> Self {
4315        // Merge the hashmap stats, summing duplicate entries.
4316        let mut policy_roam_attempts_count_by_roam_reason =
4317            v.policy_roam_attempts_count_by_roam_reason.clone();
4318        for (reason, count) in &self.policy_roam_attempts_count_by_roam_reason {
4319            let _ = policy_roam_attempts_count_by_roam_reason
4320                .entry(*reason)
4321                .and_modify(|e| *e = e.saturating_add(*count))
4322                .or_insert(*count);
4323        }
4324        let mut policy_roam_successful_count_by_roam_reason =
4325            v.policy_roam_successful_count_by_roam_reason.clone();
4326        for (reason, count) in &self.policy_roam_successful_count_by_roam_reason {
4327            let _ = policy_roam_successful_count_by_roam_reason
4328                .entry(*reason)
4329                .and_modify(|e| *e = e.saturating_add(*count))
4330                .or_insert(*count);
4331        }
4332
4333        Self {
4334            total_duration: zx::MonotonicDuration::from_nanos(
4335                self.total_duration.into_nanos().saturating_add(v.total_duration.into_nanos()),
4336            ),
4337            connected_duration: zx::MonotonicDuration::from_nanos(
4338                self.connected_duration
4339                    .into_nanos()
4340                    .saturating_add(v.connected_duration.into_nanos()),
4341            ),
4342            downtime_duration: zx::MonotonicDuration::from_nanos(
4343                self.downtime_duration
4344                    .into_nanos()
4345                    .saturating_add(v.downtime_duration.into_nanos()),
4346            ),
4347            downtime_no_saved_neighbor_duration: zx::MonotonicDuration::from_nanos(
4348                self.downtime_no_saved_neighbor_duration
4349                    .into_nanos()
4350                    .saturating_add(v.downtime_no_saved_neighbor_duration.into_nanos()),
4351            ),
4352            connect_attempts_count: self
4353                .connect_attempts_count
4354                .saturating_add(v.connect_attempts_count),
4355            connect_successful_count: self
4356                .connect_successful_count
4357                .saturating_add(v.connect_successful_count),
4358            disconnect_count: self.disconnect_count.saturating_add(v.disconnect_count),
4359            total_non_roam_disconnect_count: self
4360                .total_non_roam_disconnect_count
4361                .saturating_add(v.total_non_roam_disconnect_count),
4362            total_roam_disconnect_count: self
4363                .total_roam_disconnect_count
4364                .saturating_add(v.total_roam_disconnect_count),
4365            policy_roam_attempts_count: self
4366                .policy_roam_attempts_count
4367                .saturating_add(v.policy_roam_attempts_count),
4368            policy_roam_successful_count: self
4369                .policy_roam_successful_count
4370                .saturating_add(v.policy_roam_successful_count),
4371            policy_roam_disconnects_count: self
4372                .policy_roam_disconnects_count
4373                .saturating_add(v.policy_roam_disconnects_count),
4374            policy_roam_attempts_count_by_roam_reason,
4375            policy_roam_successful_count_by_roam_reason,
4376            tx_high_packet_drop_duration: zx::MonotonicDuration::from_nanos(
4377                self.tx_high_packet_drop_duration
4378                    .into_nanos()
4379                    .saturating_add(v.tx_high_packet_drop_duration.into_nanos()),
4380            ),
4381            rx_high_packet_drop_duration: zx::MonotonicDuration::from_nanos(
4382                self.rx_high_packet_drop_duration
4383                    .into_nanos()
4384                    .saturating_add(v.rx_high_packet_drop_duration.into_nanos()),
4385            ),
4386            tx_very_high_packet_drop_duration: zx::MonotonicDuration::from_nanos(
4387                self.tx_very_high_packet_drop_duration
4388                    .into_nanos()
4389                    .saturating_add(v.tx_very_high_packet_drop_duration.into_nanos()),
4390            ),
4391            rx_very_high_packet_drop_duration: zx::MonotonicDuration::from_nanos(
4392                self.rx_very_high_packet_drop_duration
4393                    .into_nanos()
4394                    .saturating_add(v.rx_very_high_packet_drop_duration.into_nanos()),
4395            ),
4396            no_rx_duration: zx::MonotonicDuration::from_nanos(
4397                self.no_rx_duration.into_nanos().saturating_add(v.no_rx_duration.into_nanos()),
4398            ),
4399        }
4400    }
4401}
4402
4403#[derive(Debug)]
4404struct DailyDetailedStats {
4405    connect_attempts_status: HashMap<fidl_ieee80211::StatusCode, u64>,
4406    connect_per_is_multi_bss: HashMap<
4407        metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss,
4408        ConnectAttemptsCounter,
4409    >,
4410    connect_per_security_type: HashMap<
4411        metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType,
4412        ConnectAttemptsCounter,
4413    >,
4414    connect_per_primary_channel: HashMap<u8, ConnectAttemptsCounter>,
4415    connect_per_channel_band: HashMap<
4416        metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand,
4417        ConnectAttemptsCounter,
4418    >,
4419    connect_per_rssi_bucket:
4420        HashMap<metrics::ConnectivityWlanMetricDimensionRssiBucket, ConnectAttemptsCounter>,
4421    connect_per_snr_bucket:
4422        HashMap<metrics::ConnectivityWlanMetricDimensionSnrBucket, ConnectAttemptsCounter>,
4423}
4424
4425impl DailyDetailedStats {
4426    pub fn new() -> Self {
4427        Self {
4428            connect_attempts_status: HashMap::new(),
4429            connect_per_is_multi_bss: HashMap::new(),
4430            connect_per_security_type: HashMap::new(),
4431            connect_per_primary_channel: HashMap::new(),
4432            connect_per_channel_band: HashMap::new(),
4433            connect_per_rssi_bucket: HashMap::new(),
4434            connect_per_snr_bucket: HashMap::new(),
4435        }
4436    }
4437}
4438
4439#[derive(Debug, Default, Copy, Clone, PartialEq)]
4440struct ConnectAttemptsCounter {
4441    success: u64,
4442    total: u64,
4443}
4444
4445impl ConnectAttemptsCounter {
4446    fn increment(&mut self, code: fidl_ieee80211::StatusCode) {
4447        self.total += 1;
4448        if code == fidl_ieee80211::StatusCode::Success {
4449            self.success += 1;
4450        }
4451    }
4452}
4453
4454#[cfg(test)]
4455mod tests {
4456    use super::*;
4457    use crate::util::testing::{
4458        generate_disconnect_info, generate_policy_roam_request, generate_random_ap_state,
4459        generate_random_bss, generate_random_channel, generate_random_scanned_candidate,
4460    };
4461    use assert_matches::assert_matches;
4462    use diagnostics_assertions::{
4463        AnyBoolProperty, AnyNumericProperty, AnyStringProperty, NonZeroUintProperty,
4464    };
4465    use fidl::endpoints::create_proxy_and_stream;
4466    use fidl_fuchsia_metrics::{MetricEvent, MetricEventLoggerRequest, MetricEventPayload};
4467    use fidl_fuchsia_wlan_ieee80211::WlanBand::{FiveGhz, TwoGhz};
4468    use fuchsia_inspect::reader;
4469    use futures::TryStreamExt;
4470    use futures::stream::FusedStream;
4471    use futures::task::Poll;
4472    use ieee80211_testutils::{BSSID_REGEX, SSID_REGEX};
4473    use regex::Regex;
4474    use std::collections::VecDeque;
4475    use std::pin::{Pin, pin};
4476    use test_case::test_case;
4477    use test_util::assert_gt;
4478    use wlan_common::bss::BssDescription;
4479    use wlan_common::ie::IeType;
4480    use wlan_common::test_utils::fake_stas::IesOverrides;
4481    use wlan_common::{random_bss_description, random_fidl_bss_description};
4482
4483    const IFACE_ID: u16 = 1;
4484
4485    // Macro rule for testing Inspect data tree. When we query for Inspect data, the LazyNode
4486    // will make a stats query req that we need to respond to in order to unblock the test.
4487    macro_rules! assert_data_tree_with_respond_blocking_req {
4488        ($test_helper:expr, $test_fut:expr, $($rest:tt)+) => {{
4489            use {
4490                fuchsia_inspect::reader, diagnostics_assertions::assert_data_tree,
4491            };
4492
4493            let inspector = $test_helper.inspector.clone();
4494            let read_fut = reader::read(&inspector);
4495            let mut read_fut = pin!(read_fut);
4496            loop {
4497                match $test_helper.exec.run_until_stalled(&mut read_fut) {
4498                    Poll::Pending => {
4499                        // Run telemetry test future so it can respond to QueryStatus request,
4500                        // while clearing out any potentially blocking Cobalt events
4501                        $test_helper.drain_cobalt_events(&mut $test_fut);
4502                        // Manually respond to iface stats request
4503                        $test_helper.telemetry_svc_streams.retain(|s| !s.is_terminated());
4504                        for telemetry_svc_stream in &mut $test_helper.telemetry_svc_streams {
4505                            respond_iface_histogram_stats_req(
4506                                &mut $test_helper.exec,
4507                                telemetry_svc_stream,
4508                            );
4509                        }
4510
4511                    }
4512                    Poll::Ready(result) => {
4513                        let hierarchy = result.expect("failed to get hierarchy");
4514                        assert_data_tree!(@executor $test_helper.exec, hierarchy, $($rest)+);
4515                        break
4516                    }
4517                }
4518            }
4519        }}
4520    }
4521
4522    #[fuchsia::test]
4523    fn test_detect_driver_unresponsive_signal_ind() {
4524        let (mut test_helper, mut test_fut) = setup_test();
4525        test_helper.send_connected_event(random_bss_description!(Wpa2));
4526
4527        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4528            stats: contains {
4529                is_driver_unresponsive: false,
4530            }
4531        });
4532
4533        test_helper.advance_by(
4534            UNRESPONSIVE_FLAG_MIN_DURATION - TELEMETRY_QUERY_INTERVAL,
4535            test_fut.as_mut(),
4536        );
4537        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4538            stats: contains {
4539                is_driver_unresponsive: false,
4540            }
4541        });
4542
4543        // Send a signal, which resets timing information for determining driver unresponsiveness
4544        let ind = fidl_internal::SignalReportIndication {
4545            rssi_dbm: Some(-40),
4546            snr_db: Some(30),
4547            tx_rate_500kbps: Some(0),
4548            ..Default::default()
4549        };
4550        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind });
4551
4552        test_helper.advance_by(UNRESPONSIVE_FLAG_MIN_DURATION, test_fut.as_mut());
4553        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4554            stats: contains {
4555                is_driver_unresponsive: false,
4556            }
4557        });
4558
4559        // On the next telemetry interval, driver is recognized as unresponsive
4560        test_helper.advance_by(TELEMETRY_QUERY_INTERVAL, test_fut.as_mut());
4561        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4562            stats: contains {
4563                is_driver_unresponsive: true,
4564            }
4565        });
4566    }
4567
4568    #[fuchsia::test]
4569    fn test_histogram_stats_timeout() {
4570        let mut exec = fasync::TestExecutor::new();
4571
4572        let inspector = Inspector::default();
4573        let external_node = inspector.root().create_child("external");
4574        let external_inspect_node = ExternalInspectNode::new(external_node);
4575
4576        let (telemetry_sender, mut telemetry_receiver) =
4577            mpsc::channel::<TelemetryEvent>(TELEMETRY_EVENT_BUFFER_SIZE);
4578        let (defect_sender, mut defect_receiver) = mpsc::channel(100);
4579
4580        // Setup the lazy child node.  When the inspect node is read, it will snapshot current
4581        // interface state.
4582        inspect_record_external_data(
4583            &external_inspect_node,
4584            TelemetrySender::new(telemetry_sender),
4585            defect_sender,
4586        );
4587
4588        // Initiate a read of the inspect node.  This will run the future that was constructed.
4589        let fut = reader::read(&inspector);
4590        let mut fut = pin!(fut);
4591        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4592
4593        // First, inspect will query the current state from the telemetry event loop.  In order to
4594        // get to the point of querying histograms, we need to reply that we are in the connected
4595        // state.
4596        let (telemetry_proxy, _telemetry_server) =
4597            fidl::endpoints::create_proxy::<fidl_sme::TelemetryMarker>();
4598        assert_matches!(
4599            telemetry_receiver.try_recv(),
4600            Ok(TelemetryEvent::QueryStatus {sender}) => {
4601                sender.send(QueryStatusResult {
4602                    connection_state: ConnectionStateInfo::Connected {
4603                        iface_id: 0,
4604                        ap_state: Box::new(random_bss_description!(Wpa2).into()),
4605                        telemetry_proxy: Some(telemetry_proxy)
4606                    }
4607                }).expect("failed to send query status result")
4608            }
4609        );
4610        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4611
4612        // The future should block on getting the histogram stats until the timer expires.
4613        assert!(exec.wake_next_timer().is_some());
4614        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(_));
4615
4616        // We should get a timeout defect.
4617        assert_matches!(
4618            defect_receiver.try_recv(),
4619            Ok(Defect::Iface(IfaceFailure::Timeout {
4620                iface_id: 0,
4621                source: TimeoutSource::GetHistogramStats,
4622            }))
4623        );
4624    }
4625
4626    #[fuchsia::test]
4627    fn test_telemetry_timeout() {
4628        let mut exec = fasync::TestExecutor::new();
4629
4630        // Boilerplate for creating a Telemetry struct
4631        let (sender, _receiver) = mpsc::channel::<TelemetryEvent>(TELEMETRY_EVENT_BUFFER_SIZE);
4632        let (monitor_svc_proxy, _monitor_svc_stream) =
4633            create_proxy_and_stream::<fidl_fuchsia_wlan_device_service::DeviceMonitorMarker>();
4634        let (cobalt_proxy, _cobalt_stream) =
4635            create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
4636        let inspector = Inspector::default();
4637        let inspect_node = inspector.root().create_child("stats");
4638        let external_inspect_node = inspector.root().create_child("external");
4639        let (defect_sender, mut defect_receiver) = mpsc::channel(100);
4640
4641        let mut telemetry = Telemetry::new(
4642            TelemetrySender::new(sender),
4643            monitor_svc_proxy,
4644            cobalt_proxy.clone(),
4645            inspect_node,
4646            external_inspect_node,
4647            defect_sender,
4648        );
4649
4650        // Setup the Telemetry struct so that it thinks that it is connected.
4651        let (telemetry_proxy, _telemetry_server) =
4652            fidl::endpoints::create_proxy::<fidl_sme::TelemetryMarker>();
4653        telemetry.connection_state = ConnectionState::Connected(Box::new(ConnectedState {
4654            iface_id: 0,
4655            ap_state: Box::new(random_bss_description!(Wpa2).into()),
4656            telemetry_proxy: Some(telemetry_proxy),
4657
4658            // The rest of the fields don't matter for this test case.
4659            new_connect_start_time: None,
4660            prev_connection_stats: None,
4661            multiple_bss_candidates: false,
4662            network_is_likely_hidden: false,
4663            last_signal_report: fasync::MonotonicInstant::now(),
4664            num_consecutive_get_counter_stats_failures: InspectableU64::new(
4665                0,
4666                &telemetry.inspect_node,
4667                "num_consecutive_get_counter_stats_failures",
4668            ),
4669            is_driver_unresponsive: InspectableBool::new(
4670                false,
4671                &telemetry.inspect_node,
4672                "is_driver_unresponsive",
4673            ),
4674        }));
4675
4676        // Call handle_periodic_telemetry.
4677        let fut = telemetry.handle_periodic_telemetry();
4678        let mut fut = pin!(fut);
4679        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4680
4681        // Have the executor trigger the timeout.
4682        assert!(exec.wake_next_timer().is_some());
4683        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4684
4685        // Verify that the timeout has been received.
4686        assert_matches!(
4687            defect_receiver.try_recv(),
4688            Ok(Defect::Iface(IfaceFailure::Timeout {
4689                iface_id: 0,
4690                source: TimeoutSource::GetIfaceStats,
4691            }))
4692        );
4693    }
4694
4695    #[fuchsia::test]
4696    fn test_logging_num_consecutive_get_iface_stats_failures() {
4697        let (mut test_helper, mut test_fut) = setup_test();
4698        test_helper.set_iface_stats_resp(Box::new(|| Err(zx::sys::ZX_ERR_TIMED_OUT)));
4699        test_helper.send_connected_event(random_bss_description!(Wpa2));
4700
4701        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4702            stats: contains {
4703                num_consecutive_get_counter_stats_failures: 0u64,
4704            }
4705        });
4706
4707        test_helper.advance_by(TELEMETRY_QUERY_INTERVAL * 20i64, test_fut.as_mut());
4708        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4709            stats: contains {
4710                num_consecutive_get_counter_stats_failures: 20u64,
4711            }
4712        });
4713
4714        // Expect that Cobalt has been notified.
4715        test_helper.drain_cobalt_events(&mut test_fut);
4716        let logged_metrics =
4717            test_helper.get_logged_metrics(metrics::CONSECUTIVE_COUNTER_STATS_FAILURES_METRIC_ID);
4718        assert_eq!(logged_metrics.len(), 20);
4719
4720        assert_eq!(
4721            logged_metrics[19].payload,
4722            fidl_fuchsia_metrics::MetricEventPayload::IntegerValue(20)
4723        );
4724    }
4725
4726    #[fuchsia::test]
4727    fn test_log_connect_event_correct_shape() {
4728        let (mut test_helper, mut test_fut) = setup_test();
4729        test_helper.send_connected_event(random_bss_description!(Wpa2));
4730
4731        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
4732
4733        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4734            stats: contains {
4735                connect_events: {
4736                    "0": {
4737                        "@time": AnyNumericProperty,
4738                        multiple_bss_candidates: AnyBoolProperty,
4739                        network: {
4740                            bssid: &*BSSID_REGEX,
4741                            ssid: &*SSID_REGEX,
4742                            rssi_dbm: AnyNumericProperty,
4743                            snr_db: AnyNumericProperty,
4744                        }
4745                    }
4746                }
4747            }
4748        });
4749    }
4750
4751    #[fuchsia::test]
4752    fn test_log_connection_status_correct_shape() {
4753        let (mut test_helper, mut test_fut) = setup_test();
4754        test_helper.send_connected_event(random_bss_description!(Wpa2));
4755
4756        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
4757
4758        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4759            stats: contains {
4760                connection_status: contains {
4761                    status_string: AnyStringProperty,
4762                    connected_network: contains {
4763                        rssi_dbm: AnyNumericProperty,
4764                        snr_db: AnyNumericProperty,
4765                        bssid: &*BSSID_REGEX,
4766                        ssid: &*SSID_REGEX,
4767                        protection: AnyStringProperty,
4768                        channel: AnyStringProperty,
4769                        is_wmm_assoc: AnyBoolProperty,
4770                    }
4771                }
4772            }
4773        });
4774    }
4775
4776    #[allow(clippy::regex_creation_in_loops, reason = "mass allow for https://fxbug.dev/381896734")]
4777    #[fuchsia::test]
4778    fn test_log_disconnect_event_correct_shape() {
4779        let (mut test_helper, mut test_fut) = setup_test();
4780
4781        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
4782            track_subsequent_downtime: false,
4783            info: Some(fake_disconnect_info()),
4784        });
4785        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
4786
4787        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4788            external: contains {
4789                stats: contains {
4790                    disconnect_events: {
4791                        "0": {
4792                            "@time": AnyNumericProperty,
4793                            flattened_reason_code: AnyNumericProperty,
4794                            locally_initiated: AnyBoolProperty,
4795                            network: {
4796                                channel: {
4797                                    primary: AnyNumericProperty,
4798                                }
4799                            }
4800                        }
4801                    }
4802                }
4803            },
4804            stats: contains {
4805                disconnect_events: {
4806                    "0": {
4807                        "@time": AnyNumericProperty,
4808                        connected_duration: AnyNumericProperty,
4809                        disconnect_source: Regex::new("^source: [^,]+, reason: [^,]+(?:, mlme_event_name: [^,]+)?$").unwrap(),
4810                        network: contains {
4811                            rssi_dbm: AnyNumericProperty,
4812                            snr_db: AnyNumericProperty,
4813                            bssid: &*BSSID_REGEX,
4814                            ssid: &*SSID_REGEX,
4815                            protection: AnyStringProperty,
4816                            channel: AnyStringProperty,
4817                            is_wmm_assoc: AnyBoolProperty,
4818                        }
4819                    }
4820                }
4821            }
4822        });
4823    }
4824
4825    #[fuchsia::test]
4826    fn test_log_disconnect_on_recovery() {
4827        let mut exec = fasync::TestExecutor::new();
4828
4829        // Boilerplate for creating a Telemetry struct
4830        let (sender, _receiver) = mpsc::channel::<TelemetryEvent>(TELEMETRY_EVENT_BUFFER_SIZE);
4831        let (monitor_svc_proxy, _monitor_svc_stream) =
4832            create_proxy_and_stream::<fidl_fuchsia_wlan_device_service::DeviceMonitorMarker>();
4833        let (cobalt_1dot1_proxy, mut cobalt_1dot1_stream) =
4834            create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
4835        let inspector = Inspector::default();
4836        let inspect_node = inspector.root().create_child("stats");
4837        let external_inspect_node = inspector.root().create_child("external");
4838        let (defect_sender, _defect_receiver) = mpsc::channel(100);
4839
4840        // Create a telemetry struct and initialize it to be in the connected state.
4841        let mut telemetry = Telemetry::new(
4842            TelemetrySender::new(sender),
4843            monitor_svc_proxy,
4844            cobalt_1dot1_proxy.clone(),
4845            inspect_node,
4846            external_inspect_node,
4847            defect_sender,
4848        );
4849
4850        telemetry.connection_state = ConnectionState::Connected(Box::new(ConnectedState {
4851            iface_id: 0,
4852            new_connect_start_time: None,
4853            prev_connection_stats: None,
4854            multiple_bss_candidates: false,
4855            ap_state: Box::new(generate_random_ap_state()),
4856            network_is_likely_hidden: false,
4857            last_signal_report: fasync::MonotonicInstant::now(),
4858            num_consecutive_get_counter_stats_failures: InspectableU64::new(
4859                0,
4860                &telemetry.inspect_node,
4861                "num_consecutive_get_counter_stats_failures",
4862            ),
4863            is_driver_unresponsive: InspectableBool::new(
4864                false,
4865                &telemetry.inspect_node,
4866                "is_driver_unresponsive",
4867            ),
4868            telemetry_proxy: None,
4869        }));
4870
4871        {
4872            // Send a disconnect event with empty disconnect info.
4873            let fut = telemetry.handle_telemetry_event(TelemetryEvent::Disconnected {
4874                track_subsequent_downtime: false,
4875                info: None,
4876            });
4877            let mut fut = pin!(fut);
4878
4879            assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4880
4881            // There should be a single batch logging event.
4882            assert_matches!(
4883                exec.run_until_stalled(&mut cobalt_1dot1_stream.next()),
4884                Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerRequest::LogMetricEvents {
4885                    events: _,
4886                    responder
4887                }))) => {
4888                    responder.send(Ok(())).expect("failed to send response");
4889                }
4890            );
4891
4892            // And then the future should run to completion.
4893            assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
4894        }
4895
4896        // Verify that the telemetry state has transitioned to idle.
4897        assert_matches!(
4898            telemetry.connection_state,
4899            ConnectionState::Idle(IdleState { connect_start_time: None })
4900        );
4901    }
4902
4903    #[fuchsia::test]
4904    fn test_stat_cycles() {
4905        let (mut test_helper, mut test_fut) = setup_test();
4906        test_helper.send_connected_event(random_bss_description!(Wpa2));
4907        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
4908
4909        test_helper.advance_by(
4910            zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL,
4911            test_fut.as_mut(),
4912        );
4913        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4914            stats: contains {
4915                "1d_counters": contains {
4916                    total_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
4917                    connected_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
4918                },
4919                "7d_counters": contains {
4920                    total_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
4921                    connected_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
4922                },
4923            }
4924        });
4925
4926        test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
4927        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4928            stats: contains {
4929                "1d_counters": contains {
4930                    // The first hour window is now discarded, so it only shows 23 hours
4931                    // of total and connected duration.
4932                    total_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
4933                    connected_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
4934                },
4935                "7d_counters": contains {
4936                    total_duration: zx::MonotonicDuration::from_hours(24).into_nanos(),
4937                    connected_duration: zx::MonotonicDuration::from_hours(24).into_nanos(),
4938                },
4939            }
4940        });
4941
4942        test_helper.advance_by(zx::MonotonicDuration::from_hours(2), test_fut.as_mut());
4943        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4944            stats: contains {
4945                "1d_counters": contains {
4946                    total_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
4947                    connected_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
4948                },
4949                "7d_counters": contains {
4950                    total_duration: zx::MonotonicDuration::from_hours(26).into_nanos(),
4951                    connected_duration: zx::MonotonicDuration::from_hours(26).into_nanos(),
4952                },
4953            }
4954        });
4955
4956        // Disconnect now
4957        let info = fake_disconnect_info();
4958        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
4959            track_subsequent_downtime: false,
4960            info: Some(info),
4961        });
4962        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
4963
4964        test_helper.advance_by(zx::MonotonicDuration::from_hours(8), test_fut.as_mut());
4965        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4966            stats: contains {
4967                "1d_counters": contains {
4968                    total_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
4969                    // Now the 1d connected counter should decrease
4970                    connected_duration: zx::MonotonicDuration::from_hours(15).into_nanos(),
4971                },
4972                "7d_counters": contains {
4973                    total_duration: zx::MonotonicDuration::from_hours(34).into_nanos(),
4974                    connected_duration: zx::MonotonicDuration::from_hours(26).into_nanos(),
4975                },
4976            }
4977        });
4978
4979        // The 7d counters do not decrease before the 7th day
4980        test_helper.advance_by(zx::MonotonicDuration::from_hours(14), test_fut.as_mut());
4981        test_helper.advance_by(
4982            zx::MonotonicDuration::from_hours(5 * 24) - TELEMETRY_QUERY_INTERVAL,
4983            test_fut.as_mut(),
4984        );
4985        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4986            stats: contains {
4987                "1d_counters": contains {
4988                    total_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
4989                    connected_duration: 0i64,
4990                },
4991                "7d_counters": contains {
4992                    total_duration: (zx::MonotonicDuration::from_hours(7 * 24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
4993                    connected_duration: zx::MonotonicDuration::from_hours(26).into_nanos(),
4994                },
4995            }
4996        });
4997
4998        // On the 7th day, the first window is removed (24 hours of duration is deducted)
4999        test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5000        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5001            stats: contains {
5002                "1d_counters": contains {
5003                    total_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
5004                    connected_duration: 0i64,
5005                },
5006                "7d_counters": contains {
5007                    total_duration: zx::MonotonicDuration::from_hours(6 * 24).into_nanos(),
5008                    connected_duration: zx::MonotonicDuration::from_hours(2).into_nanos(),
5009                },
5010            }
5011        });
5012    }
5013
5014    #[fuchsia::test]
5015    fn test_daily_detailed_stat_cycles() {
5016        let (mut test_helper, mut test_fut) = setup_test();
5017        for _ in 0..10 {
5018            test_helper.send_connected_event(random_bss_description!(Wpa2));
5019        }
5020        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
5021
5022        // On 1st day, 10 successful connects, so verify metric is logged with count of 10.
5023        let status_codes = test_helper.get_logged_metrics(
5024            metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
5025        );
5026        assert_eq!(status_codes.len(), 1);
5027        assert_eq!(
5028            status_codes[0].event_codes,
5029            vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
5030        );
5031        assert_eq!(status_codes[0].payload, MetricEventPayload::Count(10));
5032
5033        test_helper.cobalt_events.clear();
5034
5035        test_helper.send_connected_event(random_bss_description!(Wpa2));
5036        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
5037
5038        // On 2nd day, 1 successful connect, so verify metric is logged with count of 1.
5039        let status_codes = test_helper.get_logged_metrics(
5040            metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
5041        );
5042        assert_eq!(status_codes.len(), 1);
5043        assert_eq!(
5044            status_codes[0].event_codes,
5045            vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
5046        );
5047        assert_eq!(status_codes[0].payload, MetricEventPayload::Count(1));
5048    }
5049
5050    #[fuchsia::test]
5051    fn test_total_duration_counters() {
5052        let (mut test_helper, mut test_fut) = setup_test();
5053
5054        test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5055        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5056            stats: contains {
5057                "1d_counters": contains {
5058                    total_duration: zx::MonotonicDuration::from_minutes(30).into_nanos(),
5059                },
5060                "7d_counters": contains {
5061                    total_duration: zx::MonotonicDuration::from_minutes(30).into_nanos(),
5062                },
5063            }
5064        });
5065
5066        test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5067        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5068            stats: contains {
5069                "1d_counters": contains {
5070                    total_duration: zx::MonotonicDuration::from_hours(1).into_nanos(),
5071                },
5072                "7d_counters": contains {
5073                    total_duration: zx::MonotonicDuration::from_hours(1).into_nanos(),
5074                },
5075            }
5076        });
5077    }
5078
5079    #[fuchsia::test]
5080    fn test_counters_when_idle() {
5081        let (mut test_helper, mut test_fut) = setup_test();
5082
5083        test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5084        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5085            stats: contains {
5086                "1d_counters": contains {
5087                    connected_duration: 0i64,
5088                    downtime_duration: 0i64,
5089                    downtime_no_saved_neighbor_duration: 0i64,
5090                },
5091                "7d_counters": contains {
5092                    connected_duration: 0i64,
5093                    downtime_duration: 0i64,
5094                    downtime_no_saved_neighbor_duration: 0i64,
5095                },
5096            }
5097        });
5098
5099        test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5100        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5101            stats: contains {
5102                "1d_counters": contains {
5103                    connected_duration: 0i64,
5104                    downtime_duration: 0i64,
5105                    downtime_no_saved_neighbor_duration: 0i64,
5106                },
5107                "7d_counters": contains {
5108                    connected_duration: 0i64,
5109                    downtime_duration: 0i64,
5110                    downtime_no_saved_neighbor_duration: 0i64,
5111                },
5112            }
5113        });
5114    }
5115
5116    #[fuchsia::test]
5117    fn test_connected_counters_increase_when_connected() {
5118        let (mut test_helper, mut test_fut) = setup_test();
5119        test_helper.send_connected_event(random_bss_description!(Wpa2));
5120        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5121
5122        test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5123        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5124            stats: contains {
5125                "1d_counters": contains {
5126                    connected_duration: zx::MonotonicDuration::from_minutes(30).into_nanos(),
5127                    downtime_duration: 0i64,
5128                    downtime_no_saved_neighbor_duration: 0i64,
5129                },
5130                "7d_counters": contains {
5131                    connected_duration: zx::MonotonicDuration::from_minutes(30).into_nanos(),
5132                    downtime_duration: 0i64,
5133                    downtime_no_saved_neighbor_duration: 0i64,
5134                },
5135            }
5136        });
5137
5138        test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5139        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5140            stats: contains {
5141                "1d_counters": contains {
5142                    connected_duration: zx::MonotonicDuration::from_hours(1).into_nanos(),
5143                    downtime_duration: 0i64,
5144                    downtime_no_saved_neighbor_duration: 0i64,
5145                },
5146                "7d_counters": contains {
5147                    connected_duration: zx::MonotonicDuration::from_hours(1).into_nanos(),
5148                    downtime_duration: 0i64,
5149                    downtime_no_saved_neighbor_duration: 0i64,
5150                },
5151            }
5152        });
5153    }
5154
5155    #[fuchsia::test]
5156    fn test_downtime_counter() {
5157        let (mut test_helper, mut test_fut) = setup_test();
5158
5159        // Disconnect but not track downtime. Downtime counter should not increase.
5160        let info = fake_disconnect_info();
5161        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5162            track_subsequent_downtime: false,
5163            info: Some(info),
5164        });
5165        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5166
5167        test_helper.advance_by(zx::MonotonicDuration::from_minutes(10), test_fut.as_mut());
5168
5169        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5170            stats: contains {
5171                "1d_counters": contains {
5172                    connected_duration: 0i64,
5173                    downtime_duration: 0i64,
5174                    downtime_no_saved_neighbor_duration: 0i64,
5175                },
5176                "7d_counters": contains {
5177                    connected_duration: 0i64,
5178                    downtime_duration: 0i64,
5179                    downtime_no_saved_neighbor_duration: 0i64,
5180                },
5181            }
5182        });
5183
5184        // Disconnect and track downtime. Downtime counter should now increase
5185        let info = fake_disconnect_info();
5186        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5187            track_subsequent_downtime: true,
5188            info: Some(info),
5189        });
5190        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5191
5192        test_helper.advance_by(zx::MonotonicDuration::from_minutes(15), test_fut.as_mut());
5193
5194        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5195            stats: contains {
5196                "1d_counters": contains {
5197                    connected_duration: 0i64,
5198                    downtime_duration: zx::MonotonicDuration::from_minutes(15).into_nanos(),
5199                    downtime_no_saved_neighbor_duration: 0i64,
5200                },
5201                "7d_counters": contains {
5202                    connected_duration: 0i64,
5203                    downtime_duration: zx::MonotonicDuration::from_minutes(15).into_nanos(),
5204                    downtime_no_saved_neighbor_duration: 0i64,
5205                },
5206            }
5207        });
5208    }
5209
5210    #[fuchsia::test]
5211    fn test_counters_connect_then_disconnect() {
5212        let (mut test_helper, mut test_fut) = setup_test();
5213        test_helper.send_connected_event(random_bss_description!(Wpa2));
5214        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5215
5216        test_helper.advance_by(zx::MonotonicDuration::from_seconds(5), test_fut.as_mut());
5217
5218        // Disconnect but not track downtime. Downtime counter should not increase.
5219        let info = fake_disconnect_info();
5220        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5221            track_subsequent_downtime: true,
5222            info: Some(info),
5223        });
5224        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5225
5226        // The 5 seconds connected duration is not accounted for yet.
5227        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5228            stats: contains {
5229                "1d_counters": contains {
5230                    connected_duration: 0i64,
5231                    downtime_duration: 0i64,
5232                    downtime_no_saved_neighbor_duration: 0i64,
5233                },
5234                "7d_counters": contains {
5235                    connected_duration: 0i64,
5236                    downtime_duration: 0i64,
5237                    downtime_no_saved_neighbor_duration: 0i64,
5238                },
5239            }
5240        });
5241
5242        // At next telemetry checkpoint, `test_fut` updates the connected and downtime durations.
5243        let downtime_start = fasync::MonotonicInstant::now();
5244        test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5245        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5246            stats: contains {
5247                "1d_counters": contains {
5248                    connected_duration: zx::MonotonicDuration::from_seconds(5).into_nanos(),
5249                    downtime_duration: (fasync::MonotonicInstant::now() - downtime_start).into_nanos(),
5250                    downtime_no_saved_neighbor_duration: 0i64,
5251                },
5252                "7d_counters": contains {
5253                    connected_duration: zx::MonotonicDuration::from_seconds(5).into_nanos(),
5254                    downtime_duration: (fasync::MonotonicInstant::now() - downtime_start).into_nanos(),
5255                    downtime_no_saved_neighbor_duration: 0i64,
5256                },
5257            }
5258        });
5259    }
5260
5261    #[fuchsia::test]
5262    fn test_downtime_no_saved_neighbor_duration_counter() {
5263        let (mut test_helper, mut test_fut) = setup_test();
5264        test_helper.send_connected_event(random_bss_description!(Wpa2));
5265        test_helper.drain_cobalt_events(&mut test_fut);
5266
5267        // Disconnect and track downtime.
5268        let info = fake_disconnect_info();
5269        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5270            track_subsequent_downtime: true,
5271            info: Some(info),
5272        });
5273        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5274
5275        test_helper.advance_by(zx::MonotonicDuration::from_seconds(5), test_fut.as_mut());
5276        // Indicate that there's no saved neighbor in vicinity
5277        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
5278            network_selection_type: NetworkSelectionType::Undirected,
5279            num_candidates: Ok(0),
5280            selected_count: 0,
5281        });
5282        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5283
5284        test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5285        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5286            stats: contains {
5287                "1d_counters": contains {
5288                    connected_duration: 0i64,
5289                    downtime_duration: TELEMETRY_QUERY_INTERVAL.into_nanos(),
5290                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5291                },
5292                "7d_counters": contains {
5293                    connected_duration: 0i64,
5294                    downtime_duration: TELEMETRY_QUERY_INTERVAL.into_nanos(),
5295                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5296                },
5297            }
5298        });
5299
5300        test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5301        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5302            stats: contains {
5303                "1d_counters": contains {
5304                    connected_duration: 0i64,
5305                    downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5306                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5307                },
5308                "7d_counters": contains {
5309                    connected_duration: 0i64,
5310                    downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5311                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5312                },
5313            }
5314        });
5315
5316        test_helper.advance_by(zx::MonotonicDuration::from_seconds(5), test_fut.as_mut());
5317        // Indicate that saved neighbor has been found
5318        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
5319            network_selection_type: NetworkSelectionType::Undirected,
5320            num_candidates: Ok(1),
5321            selected_count: 0,
5322        });
5323        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5324
5325        // `downtime_no_saved_neighbor_duration` counter is not updated right away.
5326        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5327            stats: contains {
5328                "1d_counters": contains {
5329                    connected_duration: 0i64,
5330                    downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5331                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5332                },
5333                "7d_counters": contains {
5334                    connected_duration: 0i64,
5335                    downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5336                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5337                },
5338            }
5339        });
5340
5341        // At the next checkpoint, both downtime counters are updated together.
5342        test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5343        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5344            stats: contains {
5345                "1d_counters": contains {
5346                    connected_duration: 0i64,
5347                    downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
5348                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5349                },
5350                "7d_counters": contains {
5351                    connected_duration: 0i64,
5352                    downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
5353                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5354                },
5355            }
5356        });
5357
5358        // Disconnect but don't track downtime
5359        let info = fake_disconnect_info();
5360        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5361            track_subsequent_downtime: false,
5362            info: Some(info),
5363        });
5364
5365        // Indicate that there's no saved neighbor in vicinity
5366        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
5367            network_selection_type: NetworkSelectionType::Undirected,
5368            num_candidates: Ok(0),
5369            selected_count: 0,
5370        });
5371        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5372        test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5373
5374        // However, this time neither of the downtime counters should be incremented
5375        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5376            stats: contains {
5377                "1d_counters": contains {
5378                    connected_duration: 0i64,
5379                    downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
5380                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5381                },
5382                "7d_counters": contains {
5383                    connected_duration: 0i64,
5384                    downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
5385                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5386                },
5387            }
5388        });
5389    }
5390
5391    #[fuchsia::test]
5392    fn test_log_connect_attempt_counters() {
5393        let (mut test_helper, mut test_fut) = setup_test();
5394
5395        // Send 10 failed connect results, then 1 successful.
5396        for i in 0..10 {
5397            let event = TelemetryEvent::ConnectResult {
5398                iface_id: IFACE_ID,
5399                policy_connect_reason: Some(
5400                    client::types::ConnectReason::RetryAfterFailedConnectAttempt,
5401                ),
5402                result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
5403                multiple_bss_candidates: true,
5404                ap_state: random_bss_description!(Wpa1).into(),
5405                network_is_likely_hidden: false,
5406            };
5407            test_helper.telemetry_sender.send(event);
5408
5409            // Verify that the connection failure has been logged.
5410            test_helper.drain_cobalt_events(&mut test_fut);
5411            let logged_metrics =
5412                test_helper.get_logged_metrics(metrics::CONNECTION_FAILURES_METRIC_ID);
5413            assert_eq!(logged_metrics.len(), i + 1);
5414        }
5415        test_helper.send_connected_event(random_bss_description!(Wpa2));
5416        test_helper.drain_cobalt_events(&mut test_fut);
5417
5418        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5419            stats: contains {
5420                "1d_counters": contains {
5421                    connect_attempts_count: 11u64,
5422                    connect_successful_count: 1u64,
5423                },
5424                "7d_counters": contains {
5425                    connect_attempts_count: 11u64,
5426                    connect_successful_count: 1u64,
5427                },
5428            }
5429        });
5430    }
5431
5432    #[fuchsia::test]
5433    fn test_disconnect_count_counter() {
5434        let (mut test_helper, mut test_fut) = setup_test();
5435        test_helper.send_connected_event(random_bss_description!(Wpa2));
5436        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5437
5438        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5439            stats: contains {
5440                "1d_counters": contains {
5441                    disconnect_count: 0u64,
5442                    policy_roam_disconnects_count: 0u64,
5443                },
5444                "7d_counters": contains {
5445                    disconnect_count: 0u64,
5446                    policy_roam_disconnects_count: 0u64,
5447                },
5448            }
5449        });
5450
5451        let info = DisconnectInfo {
5452            disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
5453                reason_code: fidl_ieee80211::ReasonCode::StaLeaving,
5454                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DisassociateIndication,
5455            }),
5456            ..fake_disconnect_info()
5457        };
5458        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5459            track_subsequent_downtime: true,
5460            info: Some(info),
5461        });
5462        test_helper.drain_cobalt_events(&mut test_fut);
5463
5464        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5465            stats: contains {
5466                "1d_counters": contains {
5467                    disconnect_count: 1u64,
5468                    policy_roam_disconnects_count: 0u64,
5469                    total_non_roam_disconnect_count: 1u64,
5470                    total_roam_disconnect_count: 0u64,
5471                },
5472                "7d_counters": contains {
5473                    disconnect_count: 1u64,
5474                    policy_roam_disconnects_count: 0u64,
5475                    total_non_roam_disconnect_count: 1u64,
5476                    total_roam_disconnect_count: 0u64,
5477                },
5478            }
5479        });
5480
5481        let info = DisconnectInfo {
5482            disconnect_source: fidl_sme::DisconnectSource::User(
5483                fidl_sme::UserDisconnectReason::Startup,
5484            ),
5485            ..fake_disconnect_info()
5486        };
5487        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5488            track_subsequent_downtime: false,
5489            info: Some(info),
5490        });
5491        test_helper.drain_cobalt_events(&mut test_fut);
5492
5493        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5494            stats: contains {
5495                "1d_counters": contains {
5496                    disconnect_count: 2u64,
5497                    policy_roam_disconnects_count: 0u64,
5498                    total_non_roam_disconnect_count: 2u64,
5499                    total_roam_disconnect_count: 0u64,
5500                },
5501                "7d_counters": contains {
5502                    disconnect_count: 2u64,
5503                    policy_roam_disconnects_count: 0u64,
5504                    total_non_roam_disconnect_count: 2u64,
5505                    total_roam_disconnect_count: 0u64,
5506                },
5507            }
5508        });
5509
5510        // Send a firmware initiated roam disconnect.
5511        let info = DisconnectInfo {
5512            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
5513                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
5514                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
5515            }),
5516            ..fake_disconnect_info()
5517        };
5518        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5519            track_subsequent_downtime: false,
5520            info: Some(info),
5521        });
5522        test_helper.drain_cobalt_events(&mut test_fut);
5523
5524        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5525            stats: contains {
5526                "1d_counters": contains {
5527                    disconnect_count: 3u64,
5528                    policy_roam_disconnects_count: 0u64,
5529                    total_non_roam_disconnect_count: 2u64,
5530                    total_roam_disconnect_count: 1u64,
5531                },
5532                "7d_counters": contains {
5533                    disconnect_count: 3u64,
5534                    policy_roam_disconnects_count: 0u64,
5535                    total_non_roam_disconnect_count: 2u64,
5536                    total_roam_disconnect_count: 1u64,
5537                },
5538            }
5539        });
5540    }
5541
5542    #[fuchsia::test]
5543    fn test_policy_roam_disconnects_count_counter() {
5544        let (mut test_helper, mut test_fut) = setup_test();
5545        test_helper.send_connected_event(random_bss_description!(Wpa2));
5546        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5547        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5548            stats: contains {
5549                "1d_counters": contains {
5550                    disconnect_count: 0u64,
5551                    policy_roam_disconnects_count: 0u64,
5552                    total_roam_disconnect_count: 0u64,
5553                    total_non_roam_disconnect_count: 0u64,
5554                },
5555                "7d_counters": contains {
5556                    disconnect_count: 0u64,
5557                    policy_roam_disconnects_count: 0u64,
5558                    total_roam_disconnect_count: 0u64,
5559                    total_non_roam_disconnect_count: 0u64,
5560                },
5561            }
5562        });
5563
5564        // Send a successful policy initiated roam result event.
5565        let mut roam_result = fidl_sme::RoamResult {
5566            bssid: [1, 1, 1, 1, 1, 1],
5567            status_code: fidl_ieee80211::StatusCode::Success,
5568            original_association_maintained: false,
5569            bss_description: Some(Box::new(random_fidl_bss_description!())),
5570            disconnect_info: None,
5571            is_credential_rejected: false,
5572        };
5573        test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
5574            iface_id: 1,
5575            result: roam_result.clone(),
5576            updated_ap_state: generate_random_ap_state(),
5577            original_ap_state: Box::new(generate_random_ap_state()),
5578            request: Box::new(generate_policy_roam_request([1, 1, 1, 1, 1, 1].into())),
5579            request_time: fasync::MonotonicInstant::now(),
5580            result_time: fasync::MonotonicInstant::now(),
5581        });
5582        test_helper.drain_cobalt_events(&mut test_fut);
5583        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5584            stats: contains {
5585                "1d_counters": contains {
5586                    disconnect_count: 0u64,
5587                    policy_roam_disconnects_count: 1u64,
5588                    // Total roam disconnects should still be zero, as those are logged in the
5589                    // disconnect metric event, not the PolicyInitiatedRoamResult event.
5590                    total_roam_disconnect_count: 0u64,
5591                    total_non_roam_disconnect_count: 0u64,
5592                },
5593                "7d_counters": contains {
5594                    disconnect_count: 0u64,
5595                    policy_roam_disconnects_count: 1u64,
5596                    // Total roam disconnects should still be zero, as those are logged in the
5597                    // disconnect metric event, not the PolicyInitiatedRoamResult event.
5598                    total_roam_disconnect_count: 0u64,
5599                    total_non_roam_disconnect_count: 0u64,
5600                },
5601            }
5602        });
5603
5604        // Send a failed policy initiated roam result event.
5605        roam_result.status_code = fidl_ieee80211::StatusCode::RefusedReasonUnspecified;
5606        roam_result.disconnect_info = Some(Box::new(generate_disconnect_info(false)));
5607        test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
5608            iface_id: 1,
5609            result: roam_result.clone(),
5610            updated_ap_state: generate_random_ap_state(),
5611            original_ap_state: Box::new(generate_random_ap_state()),
5612            request: Box::new(generate_policy_roam_request([1, 1, 1, 1, 1, 1].into())),
5613            request_time: fasync::MonotonicInstant::now(),
5614            result_time: fasync::MonotonicInstant::now(),
5615        });
5616        test_helper.drain_cobalt_events(&mut test_fut);
5617        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5618            stats: contains {
5619                "1d_counters": contains {
5620                    disconnect_count: 0u64,
5621                    policy_roam_disconnects_count: 2u64,
5622                    // Total roam disconnects should still be zero, as those are logged in the
5623                    // disconnect metric event, not the PolicyInitiatedRoamResult event.
5624                    total_roam_disconnect_count: 0u64,
5625                    total_non_roam_disconnect_count: 0u64,
5626                },
5627                "7d_counters": contains {
5628                    disconnect_count: 0u64,
5629                    policy_roam_disconnects_count: 2u64,
5630                    // Total roam disconnects should still be zero, as those are logged in the
5631                    // disconnect metric event, not the PolicyInitiatedRoamResult event.
5632                    total_roam_disconnect_count: 0u64,
5633                    total_non_roam_disconnect_count: 0u64,
5634                },
5635            }
5636        });
5637
5638        // Send a failed policy initiated roam result with association maintained.
5639        roam_result.original_association_maintained = true;
5640        test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
5641            iface_id: 1,
5642            result: roam_result,
5643            updated_ap_state: generate_random_ap_state(),
5644            original_ap_state: Box::new(generate_random_ap_state()),
5645            request: Box::new(generate_policy_roam_request([1, 1, 1, 1, 1, 1].into())),
5646            request_time: fasync::MonotonicInstant::now(),
5647            result_time: fasync::MonotonicInstant::now(),
5648        });
5649        test_helper.drain_cobalt_events(&mut test_fut);
5650        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5651            stats: contains {
5652                "1d_counters": contains {
5653                    disconnect_count: 0u64,
5654                    policy_roam_disconnects_count: 2u64,
5655                    total_roam_disconnect_count: 0u64,
5656                    total_non_roam_disconnect_count: 0u64,
5657                },
5658                "7d_counters": contains {
5659                    disconnect_count: 0u64,
5660                    policy_roam_disconnects_count: 2u64,
5661                    total_roam_disconnect_count: 0u64,
5662                    total_non_roam_disconnect_count: 0u64,
5663                },
5664            }
5665        });
5666    }
5667
5668    #[fuchsia::test]
5669    fn test_rx_tx_counters_no_issue() {
5670        let (mut test_helper, mut test_fut) = setup_test();
5671        test_helper.send_connected_event(random_bss_description!(Wpa2));
5672        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5673
5674        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
5675        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5676            stats: contains {
5677                get_iface_stats_fail_count: 0u64,
5678                "1d_counters": contains {
5679                    tx_high_packet_drop_duration: 0i64,
5680                    rx_high_packet_drop_duration: 0i64,
5681                    tx_very_high_packet_drop_duration: 0i64,
5682                    rx_very_high_packet_drop_duration: 0i64,
5683                    no_rx_duration: 0i64,
5684                },
5685                "7d_counters": contains {
5686                    tx_high_packet_drop_duration: 0i64,
5687                    rx_high_packet_drop_duration: 0i64,
5688                    tx_very_high_packet_drop_duration: 0i64,
5689                    rx_very_high_packet_drop_duration: 0i64,
5690                    no_rx_duration: 0i64,
5691                },
5692            }
5693        });
5694    }
5695
5696    #[fuchsia::test]
5697    fn test_tx_high_packet_drop_duration_counters() {
5698        let (mut test_helper, mut test_fut) = setup_test();
5699        test_helper.set_iface_stats_resp(Box::new(|| {
5700            let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
5701            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
5702                connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
5703                    tx_total: Some(10 * seed),
5704                    tx_drop: Some(3 * seed),
5705                    ..fake_connection_stats(seed)
5706                }),
5707                ..Default::default()
5708            })
5709        }));
5710
5711        test_helper.send_connected_event(random_bss_description!(Wpa2));
5712        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5713
5714        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
5715        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5716            stats: contains {
5717                get_iface_stats_fail_count: 0u64,
5718                "1d_counters": contains {
5719                    // Deduct 15 seconds beecause there isn't packet counter to diff against in
5720                    // the first interval of telemetry
5721                    tx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5722                    rx_high_packet_drop_duration: 0i64,
5723                    tx_very_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5724                    rx_very_high_packet_drop_duration: 0i64,
5725                    no_rx_duration: 0i64,
5726                },
5727                "7d_counters": contains {
5728                    tx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5729                    rx_high_packet_drop_duration: 0i64,
5730                    tx_very_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5731                    rx_very_high_packet_drop_duration: 0i64,
5732                    no_rx_duration: 0i64,
5733                },
5734            }
5735        });
5736    }
5737
5738    #[fuchsia::test]
5739    fn test_rx_high_packet_drop_duration_counters() {
5740        let (mut test_helper, mut test_fut) = setup_test();
5741        test_helper.set_iface_stats_resp(Box::new(|| {
5742            let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
5743            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
5744                connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
5745                    rx_unicast_total: Some(10 * seed),
5746                    rx_unicast_drop: Some(3 * seed),
5747                    ..fake_connection_stats(seed)
5748                }),
5749                ..Default::default()
5750            })
5751        }));
5752
5753        test_helper.send_connected_event(random_bss_description!(Wpa2));
5754        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5755
5756        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
5757        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5758            stats: contains {
5759                get_iface_stats_fail_count: 0u64,
5760                "1d_counters": contains {
5761                    // Deduct 15 seconds beecause there isn't packet counter to diff against in
5762                    // the first interval of telemetry
5763                    rx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5764                    tx_high_packet_drop_duration: 0i64,
5765                    rx_very_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5766                    tx_very_high_packet_drop_duration: 0i64,
5767                    no_rx_duration: 0i64,
5768                },
5769                "7d_counters": contains {
5770                    rx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5771                    tx_high_packet_drop_duration: 0i64,
5772                    rx_very_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5773                    tx_very_high_packet_drop_duration: 0i64,
5774                    no_rx_duration: 0i64,
5775                },
5776            }
5777        });
5778    }
5779
5780    #[fuchsia::test]
5781    fn test_rx_tx_high_but_not_very_high_packet_drop_duration_counters() {
5782        let (mut test_helper, mut test_fut) = setup_test();
5783        test_helper.set_iface_stats_resp(Box::new(|| {
5784            let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
5785            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
5786                connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
5787                    // 3% drop rate would be high, but not very high
5788                    rx_unicast_total: Some(100 * seed),
5789                    rx_unicast_drop: Some(3 * seed),
5790                    tx_total: Some(100 * seed),
5791                    tx_drop: Some(3 * seed),
5792                    ..fake_connection_stats(seed)
5793                }),
5794                ..Default::default()
5795            })
5796        }));
5797
5798        test_helper.send_connected_event(random_bss_description!(Wpa2));
5799        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5800
5801        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
5802        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5803            stats: contains {
5804                get_iface_stats_fail_count: 0u64,
5805                "1d_counters": contains {
5806                    // Deduct 15 seconds beecause there isn't packet counter to diff against in
5807                    // the first interval of telemetry
5808                    rx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5809                    tx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5810                    // Very high drop rate counters should still be 0
5811                    rx_very_high_packet_drop_duration: 0i64,
5812                    tx_very_high_packet_drop_duration: 0i64,
5813                    no_rx_duration: 0i64,
5814                },
5815                "7d_counters": contains {
5816                    rx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5817                    tx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5818                    rx_very_high_packet_drop_duration: 0i64,
5819                    tx_very_high_packet_drop_duration: 0i64,
5820                    no_rx_duration: 0i64,
5821                },
5822            }
5823        });
5824    }
5825
5826    #[fuchsia::test]
5827    fn test_rx_tx_reset() {
5828        let (mut test_helper, mut test_fut) = setup_test();
5829        test_helper.set_iface_stats_resp(Box::new(|| {
5830            let seed = (fasync::MonotonicInstant::now() - fasync::MonotonicInstant::from_nanos(0))
5831                .into_seconds() as u64;
5832            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
5833                connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
5834                    rx_unicast_total: Some(999999 - seed),
5835                    rx_unicast_drop: Some(999999 - seed),
5836                    tx_total: Some(999999 - seed),
5837                    tx_drop: Some(999999 - seed),
5838                    ..fake_connection_stats(seed)
5839                }),
5840                ..Default::default()
5841            })
5842        }));
5843
5844        test_helper.send_connected_event(random_bss_description!(Wpa2));
5845        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5846
5847        // Verify there's no crash
5848        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
5849        // Verify that counters are not incremented
5850        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5851            stats: contains {
5852                get_iface_stats_fail_count: 0u64,
5853                "1d_counters": contains {
5854                    // Deduct 15 seconds because there isn't packet counter to diff against in
5855                    // the first interval of telemetry
5856                    rx_high_packet_drop_duration: 0i64,
5857                    tx_high_packet_drop_duration: 0i64,
5858                    // Very high drop rate counters should still be 0
5859                    rx_very_high_packet_drop_duration: 0i64,
5860                    tx_very_high_packet_drop_duration: 0i64,
5861                    no_rx_duration: 0i64,
5862                },
5863                "7d_counters": contains {
5864                    rx_high_packet_drop_duration: 0i64,
5865                    tx_high_packet_drop_duration: 0i64,
5866                    rx_very_high_packet_drop_duration: 0i64,
5867                    tx_very_high_packet_drop_duration: 0i64,
5868                    no_rx_duration: 0i64,
5869                },
5870            }
5871        });
5872    }
5873
5874    #[fuchsia::test]
5875    fn test_no_rx_duration_counters() {
5876        let (mut test_helper, mut test_fut) = setup_test();
5877        test_helper.set_iface_stats_resp(Box::new(|| {
5878            let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
5879            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
5880                connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
5881                    rx_unicast_total: Some(10),
5882                    ..fake_connection_stats(seed)
5883                }),
5884                ..Default::default()
5885            })
5886        }));
5887
5888        test_helper.send_connected_event(random_bss_description!(Wpa2));
5889        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5890
5891        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
5892        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5893            stats: contains {
5894                get_iface_stats_fail_count: 0u64,
5895                "1d_counters": contains {
5896                    // Deduct 15 seconds beecause there isn't packet counter to diff against in
5897                    // the first interval of telemetry
5898                    no_rx_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5899                    rx_high_packet_drop_duration: 0i64,
5900                    tx_high_packet_drop_duration: 0i64,
5901                    rx_very_high_packet_drop_duration: 0i64,
5902                    tx_very_high_packet_drop_duration: 0i64,
5903                },
5904                "7d_counters": contains {
5905                    no_rx_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5906                    rx_high_packet_drop_duration: 0i64,
5907                    tx_high_packet_drop_duration: 0i64,
5908                    rx_very_high_packet_drop_duration: 0i64,
5909                    tx_very_high_packet_drop_duration: 0i64,
5910                },
5911            }
5912        });
5913    }
5914
5915    #[fuchsia::test]
5916    fn test_get_iface_stats_fail() {
5917        let (mut test_helper, mut test_fut) = setup_test();
5918        test_helper.set_iface_stats_resp(Box::new(|| Err(zx::sys::ZX_ERR_NOT_SUPPORTED)));
5919
5920        test_helper.send_connected_event(random_bss_description!(Wpa2));
5921        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5922
5923        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
5924        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5925            stats: contains {
5926                get_iface_stats_fail_count: NonZeroUintProperty,
5927                "1d_counters": contains {
5928                    no_rx_duration: 0i64,
5929                    rx_high_packet_drop_duration: 0i64,
5930                    tx_high_packet_drop_duration: 0i64,
5931                    rx_very_high_packet_drop_duration: 0i64,
5932                    tx_very_high_packet_drop_duration: 0i64,
5933                },
5934                "7d_counters": contains {
5935                    no_rx_duration: 0i64,
5936                    rx_high_packet_drop_duration: 0i64,
5937                    tx_high_packet_drop_duration: 0i64,
5938                    rx_very_high_packet_drop_duration: 0i64,
5939                    tx_very_high_packet_drop_duration: 0i64,
5940                },
5941            }
5942        });
5943    }
5944
5945    #[fuchsia::test]
5946    fn test_log_signal_histograms_inspect() {
5947        let (mut test_helper, mut test_fut) = setup_test();
5948        test_helper.send_connected_event(random_bss_description!(Wpa2));
5949        test_helper.drain_cobalt_events(&mut test_fut);
5950
5951        // Default iface stats responder in `test_helper` already mock these histograms.
5952        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5953            external: contains {
5954                stats: contains {
5955                    connection_status: contains {
5956                        histograms: {
5957                            antenna0_2Ghz: {
5958                                antenna_index: 0u64,
5959                                antenna_freq: "2Ghz",
5960                                snr_histogram: vec![30i64, 999],
5961                                snr_invalid_samples: 11u64,
5962                                noise_floor_histogram: vec![-55i64, 999],
5963                                noise_floor_invalid_samples: 44u64,
5964                                rssi_histogram: vec![-25i64, 999],
5965                                rssi_invalid_samples: 55u64,
5966                            },
5967                            antenna1_5Ghz: {
5968                                antenna_index: 1u64,
5969                                antenna_freq: "5Ghz",
5970                                rx_rate_histogram: vec![100i64, 1500],
5971                                rx_rate_invalid_samples: 33u64,
5972                            },
5973                        }
5974                    }
5975                }
5976            }
5977        });
5978    }
5979
5980    #[fuchsia::test]
5981    fn test_log_daily_uptime_ratio_cobalt_metric() {
5982        let (mut test_helper, mut test_fut) = setup_test();
5983        test_helper.send_connected_event(random_bss_description!(Wpa2));
5984        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5985
5986        test_helper.advance_by(zx::MonotonicDuration::from_hours(12), test_fut.as_mut());
5987
5988        let info = fake_disconnect_info();
5989        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5990            track_subsequent_downtime: true,
5991            info: Some(info),
5992        });
5993        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5994
5995        test_helper.advance_by(zx::MonotonicDuration::from_hours(6), test_fut.as_mut());
5996
5997        // Indicate that there's no saved neighbor in vicinity
5998        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
5999            network_selection_type: NetworkSelectionType::Undirected,
6000            num_candidates: Ok(0),
6001            selected_count: 0,
6002        });
6003
6004        test_helper.advance_by(zx::MonotonicDuration::from_hours(6), test_fut.as_mut());
6005
6006        let uptime_ratios =
6007            test_helper.get_logged_metrics(metrics::CONNECTED_UPTIME_RATIO_METRIC_ID);
6008        assert_eq!(uptime_ratios.len(), 1);
6009        // 12 hours of uptime, 6 hours of adjusted downtime => 66.66% uptime
6010        assert_eq!(uptime_ratios[0].payload, MetricEventPayload::IntegerValue(6666));
6011    }
6012
6013    /// Send a random connect event and 4 hours later send a disconnect with the specified
6014    /// disconnect source.
6015    fn connect_and_disconnect_with_source(
6016        test_helper: &mut TestHelper,
6017        mut test_fut: Pin<&mut impl Future<Output = ()>>,
6018        disconnect_source: fidl_sme::DisconnectSource,
6019    ) {
6020        test_helper.send_connected_event(random_bss_description!(Wpa2));
6021        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6022
6023        test_helper.advance_by(zx::MonotonicDuration::from_hours(6), test_fut.as_mut());
6024
6025        let info = DisconnectInfo { disconnect_source, ..fake_disconnect_info() };
6026        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6027            track_subsequent_downtime: true,
6028            info: Some(info),
6029        });
6030        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6031        test_helper.drain_cobalt_events(&mut test_fut);
6032    }
6033
6034    #[fuchsia::test]
6035    fn test_log_daily_disconnect_per_day_connected_cobalt_metric() {
6036        let (mut test_helper, mut test_fut) = setup_test();
6037
6038        // Send 1 disconnect and 1 roaming disconnect with the device connected for a
6039        // total of 12 of 24 hours.
6040        let mlme_non_roam_source = fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
6041            reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
6042            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
6043        });
6044        connect_and_disconnect_with_source(
6045            &mut test_helper,
6046            test_fut.as_mut(),
6047            mlme_non_roam_source,
6048        );
6049
6050        let mlme_roam_source = fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
6051            reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
6052            mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamConfirmation,
6053        });
6054        connect_and_disconnect_with_source(&mut test_helper, test_fut.as_mut(), mlme_roam_source);
6055
6056        test_helper.advance_by(zx::MonotonicDuration::from_hours(12), test_fut.as_mut());
6057
6058        let dpdc_ratios =
6059            test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6060        assert_eq!(dpdc_ratios.len(), 1);
6061        // 2 disconnects, 0.5 day connected => 4 disconnects per day connected
6062        assert_eq!(dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(40_000));
6063
6064        // 1 non-roaming disconnect, 0.5 day connected => 2 non-roam disconnects per day connected
6065        let non_roam_dpdc_ratios = test_helper
6066            .get_logged_metrics(metrics::NON_ROAM_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6067        assert_eq!(non_roam_dpdc_ratios.len(), 1);
6068        assert_eq!(non_roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(20_000));
6069
6070        // Roam disconnects get logged in the roam result event, so this shouldn't have any, even
6071        // though we directly sent the roam disconnect..
6072        let roam_dpdc_ratios = test_helper
6073            .get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_PER_DAY_CONNECTED_METRIC_ID);
6074        assert_eq!(roam_dpdc_ratios.len(), 1);
6075        assert_eq!(roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6076
6077        let dpdc_ratios_7d =
6078            test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_7D_METRIC_ID);
6079        assert_eq!(dpdc_ratios_7d.len(), 1);
6080        assert_eq!(dpdc_ratios_7d[0].payload, MetricEventPayload::IntegerValue(40_000));
6081
6082        // Clear record of logged Cobalt events
6083        test_helper.cobalt_events.clear();
6084
6085        // Connect for another 1 day to dilute the 7d ratio
6086        test_helper.send_connected_event(random_bss_description!(Wpa2));
6087        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6088
6089        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
6090
6091        // No disconnect in the last day, so the 1d ratio would be 0 for all types.
6092        let dpdc_ratios =
6093            test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6094        assert_eq!(dpdc_ratios.len(), 1);
6095        assert_eq!(dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6096
6097        let non_roam_dpdc_ratios = test_helper
6098            .get_logged_metrics(metrics::NON_ROAM_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6099        assert_eq!(non_roam_dpdc_ratios.len(), 1);
6100        assert_eq!(non_roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6101
6102        let roam_dpdc_ratios = test_helper
6103            .get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_PER_DAY_CONNECTED_METRIC_ID);
6104        assert_eq!(roam_dpdc_ratios.len(), 1);
6105        assert_eq!(roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6106
6107        let dpdc_ratios_7d =
6108            test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_7D_METRIC_ID);
6109        assert_eq!(dpdc_ratios_7d.len(), 1);
6110        // The original 2 disconnects, now with 1.5 day connected => 1.333 disconnects per day
6111        // connected (which equals 13,333 in TenThousandth unit)
6112        assert_eq!(dpdc_ratios_7d[0].payload, MetricEventPayload::IntegerValue(13_333));
6113    }
6114
6115    #[fuchsia::test]
6116    fn test_log_daily_policy_roam_disconnect_per_day_connected_cobalt_metric() {
6117        let (mut test_helper, mut test_fut) = setup_test();
6118        test_helper.send_connected_event(random_bss_description!(Wpa2));
6119        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6120
6121        // Send one successful roam result
6122        let bss_desc = random_fidl_bss_description!();
6123        let roam_result = fidl_sme::RoamResult {
6124            bssid: [1, 1, 1, 1, 1, 1],
6125            status_code: fidl_ieee80211::StatusCode::Success,
6126            original_association_maintained: false,
6127            bss_description: Some(Box::new(bss_desc.clone())),
6128            disconnect_info: None,
6129            is_credential_rejected: false,
6130        };
6131        test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
6132            iface_id: 1,
6133            result: roam_result,
6134            updated_ap_state: generate_random_ap_state(),
6135            original_ap_state: Box::new(generate_random_ap_state()),
6136            request: Box::new(generate_policy_roam_request([1, 1, 1, 1, 1, 1].into())),
6137            request_time: fasync::MonotonicInstant::now(),
6138            result_time: fasync::MonotonicInstant::now(),
6139        });
6140        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6141        test_helper.advance_by(zx::MonotonicDuration::from_hours(12), test_fut.as_mut());
6142
6143        // Send a second successful roam result
6144        let bss_desc = random_fidl_bss_description!();
6145        let roam_result = fidl_sme::RoamResult {
6146            bssid: [2, 2, 2, 2, 2, 2],
6147            status_code: fidl_ieee80211::StatusCode::Success,
6148            original_association_maintained: false,
6149            bss_description: Some(Box::new(bss_desc.clone())),
6150            disconnect_info: None,
6151            is_credential_rejected: false,
6152        };
6153        test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
6154            iface_id: 1,
6155            result: roam_result,
6156            updated_ap_state: generate_random_ap_state(),
6157            original_ap_state: Box::new(generate_random_ap_state()),
6158            request: Box::new(generate_policy_roam_request([2, 2, 2, 2, 2, 2].into())),
6159            request_time: fasync::MonotonicInstant::now(),
6160            result_time: fasync::MonotonicInstant::now(),
6161        });
6162        // Send a disconnect
6163        let info = DisconnectInfo {
6164            disconnect_source: fidl_sme::DisconnectSource::User(
6165                fidl_sme::UserDisconnectReason::Unknown,
6166            ),
6167            ..fake_disconnect_info()
6168        };
6169        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6170            track_subsequent_downtime: true,
6171            info: Some(info),
6172        });
6173        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6174        test_helper.advance_by(zx::MonotonicDuration::from_hours(12), test_fut.as_mut());
6175
6176        let dpdc_ratios =
6177            test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6178        assert_eq!(dpdc_ratios.len(), 1);
6179        // 1 disconnect, 0.5 day connected => 2 disconnects per day connected
6180        // (which equals 20_0000 in TenThousandth unit)
6181        assert_eq!(dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(20_000));
6182
6183        // 2 roam disconnects, 0.4 day connected => 4 roam disconnects per day connected
6184        let roam_dpdc_ratios = test_helper
6185            .get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_PER_DAY_CONNECTED_METRIC_ID);
6186        assert_eq!(roam_dpdc_ratios.len(), 1);
6187        assert_eq!(roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(40_000));
6188    }
6189
6190    #[fuchsia::test]
6191    fn test_log_daily_disconnect_per_day_connected_cobalt_metric_device_high_disconnect() {
6192        let (mut test_helper, mut test_fut) = setup_test();
6193        test_helper.send_connected_event(random_bss_description!(Wpa2));
6194        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6195
6196        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6197        let info = fake_disconnect_info();
6198        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6199            track_subsequent_downtime: true,
6200            info: Some(info),
6201        });
6202        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6203
6204        test_helper.advance_by(zx::MonotonicDuration::from_hours(23), test_fut.as_mut());
6205    }
6206
6207    #[fuchsia::test]
6208    fn test_log_daily_rx_tx_ratio_cobalt_metrics() {
6209        let (mut test_helper, mut test_fut) = setup_test();
6210        test_helper.set_iface_stats_resp(Box::new(|| {
6211            let seed = fasync::MonotonicInstant::now().into_nanos() as u64 / 1_000_000_000;
6212            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6213                connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6214                    tx_total: Some(10 * seed),
6215                    // TX drop rate stops increasing at 1 hour + TELEMETRY_QUERY_INTERVAL mark.
6216                    // Because the first TELEMETRY_QUERY_INTERVAL doesn't count when
6217                    // computing counters, this leads to 3 hour of high TX drop rate.
6218                    tx_drop: Some(
6219                        3 * min(
6220                            seed,
6221                            (zx::MonotonicDuration::from_hours(3) + TELEMETRY_QUERY_INTERVAL)
6222                                .into_seconds() as u64,
6223                        ),
6224                    ),
6225                    // RX total stops increasing at 23 hour mark
6226                    rx_unicast_total: Some(
6227                        10 * min(seed, zx::MonotonicDuration::from_hours(23).into_seconds() as u64),
6228                    ),
6229                    // RX drop rate stops increasing at 4 hour + TELEMETRY_QUERY_INTERVAL mark.
6230                    rx_unicast_drop: Some(
6231                        3 * min(
6232                            seed,
6233                            (zx::MonotonicDuration::from_hours(4) + TELEMETRY_QUERY_INTERVAL)
6234                                .into_seconds() as u64,
6235                        ),
6236                    ),
6237                    ..fake_connection_stats(seed)
6238                }),
6239                ..Default::default()
6240            })
6241        }));
6242
6243        test_helper.send_connected_event(random_bss_description!(Wpa2));
6244        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6245
6246        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
6247
6248        let high_rx_drop_time_ratios =
6249            test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_RX_PACKET_DROP_METRIC_ID);
6250        // 4 hours of high RX drop rate, 24 hours connected => 16.66% duration
6251        assert_eq!(high_rx_drop_time_ratios.len(), 1);
6252        assert_eq!(high_rx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(1666));
6253
6254        let high_tx_drop_time_ratios =
6255            test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_TX_PACKET_DROP_METRIC_ID);
6256        // 3 hours of high RX drop rate, 24 hours connected => 12.48% duration
6257        assert_eq!(high_tx_drop_time_ratios.len(), 1);
6258        assert_eq!(high_tx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(1250));
6259
6260        let very_high_rx_drop_time_ratios = test_helper
6261            .get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID);
6262        assert_eq!(very_high_rx_drop_time_ratios.len(), 1);
6263        assert_eq!(
6264            very_high_rx_drop_time_ratios[0].payload,
6265            MetricEventPayload::IntegerValue(1666)
6266        );
6267
6268        let very_high_tx_drop_time_ratios = test_helper
6269            .get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID);
6270        assert_eq!(very_high_tx_drop_time_ratios.len(), 1);
6271        assert_eq!(
6272            very_high_tx_drop_time_ratios[0].payload,
6273            MetricEventPayload::IntegerValue(1250)
6274        );
6275
6276        // 1 hour of no RX, 24 hours connected => 4.16% duration
6277        let no_rx_time_ratios =
6278            test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_NO_RX_METRIC_ID);
6279        assert_eq!(no_rx_time_ratios.len(), 1);
6280        assert_eq!(no_rx_time_ratios[0].payload, MetricEventPayload::IntegerValue(416));
6281    }
6282
6283    #[fuchsia::test]
6284    fn test_log_daily_rx_tx_ratio_cobalt_metrics_zero() {
6285        // This test is to verify that when the RX/TX ratios are 0 (there's no issue), we still
6286        // log to Cobalt.
6287        let (mut test_helper, mut test_fut) = setup_test();
6288
6289        test_helper.send_connected_event(random_bss_description!(Wpa2));
6290        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6291
6292        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
6293
6294        let high_rx_drop_time_ratios =
6295            test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_RX_PACKET_DROP_METRIC_ID);
6296        assert_eq!(high_rx_drop_time_ratios.len(), 1);
6297        assert_eq!(high_rx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6298
6299        let high_tx_drop_time_ratios =
6300            test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_TX_PACKET_DROP_METRIC_ID);
6301        assert_eq!(high_tx_drop_time_ratios.len(), 1);
6302        assert_eq!(high_tx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6303
6304        let very_high_rx_drop_time_ratios = test_helper
6305            .get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID);
6306        assert_eq!(very_high_rx_drop_time_ratios.len(), 1);
6307        assert_eq!(very_high_rx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6308
6309        let very_high_tx_drop_time_ratios = test_helper
6310            .get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID);
6311        assert_eq!(very_high_tx_drop_time_ratios.len(), 1);
6312        assert_eq!(very_high_tx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6313
6314        let no_rx_time_ratios =
6315            test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_NO_RX_METRIC_ID);
6316        assert_eq!(no_rx_time_ratios.len(), 1);
6317        assert_eq!(no_rx_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6318    }
6319
6320    #[fuchsia::test]
6321    fn test_log_daily_establish_connection_metrics() {
6322        let (mut test_helper, mut test_fut) = setup_test();
6323
6324        // Send 10 failed connect results, then 1 successful.
6325        for _ in 0..10 {
6326            let event = TelemetryEvent::ConnectResult {
6327                iface_id: IFACE_ID,
6328                policy_connect_reason: Some(
6329                    client::types::ConnectReason::RetryAfterFailedConnectAttempt,
6330                ),
6331                result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
6332                multiple_bss_candidates: true,
6333                ap_state: random_bss_description!(Wpa1).into(),
6334                network_is_likely_hidden: true,
6335            };
6336            test_helper.telemetry_sender.send(event);
6337        }
6338        test_helper.send_connected_event(random_bss_description!(Wpa2));
6339
6340        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
6341
6342        let connection_success_rate =
6343            test_helper.get_logged_metrics(metrics::CONNECTION_SUCCESS_RATE_METRIC_ID);
6344        assert_eq!(connection_success_rate.len(), 1);
6345        // 1 successful, 11 total attempts => 9.09% success rate
6346        assert_eq!(connection_success_rate[0].payload, MetricEventPayload::IntegerValue(909));
6347    }
6348
6349    #[fuchsia::test]
6350    fn test_log_hourly_fleetwide_uptime_cobalt_metrics() {
6351        let (mut test_helper, mut test_fut) = setup_test();
6352
6353        test_helper.send_connected_event(random_bss_description!(Wpa2));
6354        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6355
6356        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6357
6358        let total_wlan_uptime_durs =
6359            test_helper.get_logged_metrics(metrics::TOTAL_WLAN_UPTIME_NEAR_SAVED_NETWORK_METRIC_ID);
6360        assert_eq!(total_wlan_uptime_durs.len(), 1);
6361        assert_eq!(
6362            total_wlan_uptime_durs[0].payload,
6363            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_hours(1).into_micros())
6364        );
6365
6366        let connected_durs =
6367            test_helper.get_logged_metrics(metrics::TOTAL_CONNECTED_UPTIME_METRIC_ID);
6368        assert_eq!(connected_durs.len(), 1);
6369        assert_eq!(
6370            connected_durs[0].payload,
6371            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_hours(1).into_micros())
6372        );
6373
6374        // Clear record of logged Cobalt events
6375        test_helper.cobalt_events.clear();
6376
6377        test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
6378
6379        let info = fake_disconnect_info();
6380        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6381            track_subsequent_downtime: true,
6382            info: Some(info),
6383        });
6384        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6385
6386        test_helper.advance_by(zx::MonotonicDuration::from_minutes(15), test_fut.as_mut());
6387
6388        // Indicate that there's no saved neighbor in vicinity
6389        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
6390            network_selection_type: NetworkSelectionType::Undirected,
6391            num_candidates: Ok(0),
6392            selected_count: 0,
6393        });
6394        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6395
6396        test_helper.advance_by(zx::MonotonicDuration::from_minutes(15), test_fut.as_mut());
6397
6398        let total_wlan_uptime_durs =
6399            test_helper.get_logged_metrics(metrics::TOTAL_WLAN_UPTIME_NEAR_SAVED_NETWORK_METRIC_ID);
6400        assert_eq!(total_wlan_uptime_durs.len(), 1);
6401        // 30 minutes connected uptime + 15 minutes downtime near saved network
6402        assert_eq!(
6403            total_wlan_uptime_durs[0].payload,
6404            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(45).into_micros())
6405        );
6406
6407        let connected_durs =
6408            test_helper.get_logged_metrics(metrics::TOTAL_CONNECTED_UPTIME_METRIC_ID);
6409        assert_eq!(connected_durs.len(), 1);
6410        assert_eq!(
6411            connected_durs[0].payload,
6412            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(30).into_micros())
6413        );
6414    }
6415
6416    #[fuchsia::test]
6417    fn test_log_hourly_fleetwide_rx_tx_cobalt_metrics() {
6418        let (mut test_helper, mut test_fut) = setup_test();
6419        test_helper.set_iface_stats_resp(Box::new(|| {
6420            let seed = fasync::MonotonicInstant::now().into_nanos() as u64 / 1_000_000_000;
6421            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6422                connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6423                    tx_total: Some(10 * seed),
6424                    // TX drop rate stops increasing at 10 min + TELEMETRY_QUERY_INTERVAL mark.
6425                    // Because the first TELEMETRY_QUERY_INTERVAL doesn't count when
6426                    // computing counters, this leads to 10 min of high TX drop rate.
6427                    tx_drop: Some(
6428                        3 * min(
6429                            seed,
6430                            (zx::MonotonicDuration::from_minutes(10) + TELEMETRY_QUERY_INTERVAL)
6431                                .into_seconds() as u64,
6432                        ),
6433                    ),
6434                    // RX total stops increasing at 45 min mark
6435                    rx_unicast_total: Some(
6436                        10 * min(
6437                            seed,
6438                            zx::MonotonicDuration::from_minutes(45).into_seconds() as u64,
6439                        ),
6440                    ),
6441                    // RX drop rate stops increasing at 20 min + TELEMETRY_QUERY_INTERVAL mark.
6442                    rx_unicast_drop: Some(
6443                        3 * min(
6444                            seed,
6445                            (zx::MonotonicDuration::from_minutes(20) + TELEMETRY_QUERY_INTERVAL)
6446                                .into_seconds() as u64,
6447                        ),
6448                    ),
6449                    ..fake_connection_stats(seed)
6450                }),
6451                ..Default::default()
6452            })
6453        }));
6454
6455        test_helper.send_connected_event(random_bss_description!(Wpa2));
6456        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6457
6458        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6459
6460        let rx_high_drop_durs =
6461            test_helper.get_logged_metrics(metrics::TOTAL_TIME_WITH_HIGH_RX_PACKET_DROP_METRIC_ID);
6462        assert_eq!(rx_high_drop_durs.len(), 1);
6463        assert_eq!(
6464            rx_high_drop_durs[0].payload,
6465            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(20).into_micros())
6466        );
6467
6468        let tx_high_drop_durs =
6469            test_helper.get_logged_metrics(metrics::TOTAL_TIME_WITH_HIGH_TX_PACKET_DROP_METRIC_ID);
6470        assert_eq!(tx_high_drop_durs.len(), 1);
6471        assert_eq!(
6472            tx_high_drop_durs[0].payload,
6473            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(10).into_micros())
6474        );
6475
6476        let rx_very_high_drop_durs = test_helper
6477            .get_logged_metrics(metrics::TOTAL_TIME_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID);
6478        assert_eq!(rx_very_high_drop_durs.len(), 1);
6479        assert_eq!(
6480            rx_very_high_drop_durs[0].payload,
6481            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(20).into_micros())
6482        );
6483
6484        let tx_very_high_drop_durs = test_helper
6485            .get_logged_metrics(metrics::TOTAL_TIME_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID);
6486        assert_eq!(tx_very_high_drop_durs.len(), 1);
6487        assert_eq!(
6488            tx_very_high_drop_durs[0].payload,
6489            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(10).into_micros())
6490        );
6491
6492        let no_rx_durs = test_helper.get_logged_metrics(metrics::TOTAL_TIME_WITH_NO_RX_METRIC_ID);
6493        assert_eq!(no_rx_durs.len(), 1);
6494        assert_eq!(
6495            no_rx_durs[0].payload,
6496            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(15).into_micros())
6497        );
6498    }
6499
6500    #[fuchsia::test]
6501    fn test_log_rssi_hourly() {
6502        let (mut test_helper, mut test_fut) = setup_test();
6503
6504        // RSSI velocity is only logged if in the connected state.
6505        test_helper.send_connected_event(random_bss_description!(Wpa2));
6506
6507        // Send some RSSI velocities
6508        let ind_1 = fidl_internal::SignalReportIndication {
6509            rssi_dbm: Some(-50),
6510            snr_db: Some(30),
6511            tx_rate_500kbps: Some(0),
6512            ..Default::default()
6513        };
6514        let ind_2 = fidl_internal::SignalReportIndication {
6515            rssi_dbm: Some(-61),
6516            snr_db: Some(40),
6517            tx_rate_500kbps: Some(0),
6518            ..Default::default()
6519        };
6520        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_1.clone() });
6521        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_1 });
6522        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_2 });
6523
6524        // After an hour has passed, the RSSI should be logged to cobalt
6525        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6526        test_helper.drain_cobalt_events(&mut test_fut);
6527
6528        let metrics = test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_METRIC_ID);
6529        assert_eq!(metrics.len(), 1);
6530        assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => {
6531            assert_eq!(buckets.len(), 2);
6532            assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 79, count: 2}));
6533            assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 68, count: 1}));
6534        });
6535        test_helper.clear_cobalt_events();
6536
6537        // Send another different RSSI
6538        let ind_3 = fidl_internal::SignalReportIndication {
6539            rssi_dbm: Some(-75),
6540            snr_db: Some(30),
6541            tx_rate_500kbps: Some(0),
6542            ..Default::default()
6543        };
6544        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_3 });
6545        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6546
6547        // Check that the previously logged values are not logged again, and the new value is
6548        // logged.
6549        test_helper.drain_cobalt_events(&mut test_fut);
6550
6551        let metrics = test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_METRIC_ID);
6552        assert_eq!(metrics.len(), 1);
6553        let buckets =
6554            assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => buckets);
6555        assert_eq!(buckets.len(), 1);
6556        assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 54, count: 1 }));
6557    }
6558
6559    #[fuchsia::test]
6560    fn test_log_rssi_velocity_hourly() {
6561        let (mut test_helper, mut test_fut) = setup_test();
6562
6563        // RSSI velocity is only logged if in the connected state.
6564        test_helper.send_connected_event(random_bss_description!(Wpa2));
6565
6566        // Send some RSSI velocities
6567        let rssi_velocity_1 = -2.0;
6568        let rssi_velocity_2 = 2.0;
6569        test_helper
6570            .telemetry_sender
6571            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: rssi_velocity_1 });
6572        test_helper
6573            .telemetry_sender
6574            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: rssi_velocity_2 });
6575        test_helper
6576            .telemetry_sender
6577            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: rssi_velocity_2 });
6578
6579        // After an hour has passed, the RSSI velocity should be logged to cobalt
6580        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6581        test_helper.drain_cobalt_events(&mut test_fut);
6582
6583        let metrics = test_helper.get_logged_metrics(metrics::RSSI_VELOCITY_METRIC_ID);
6584        assert_eq!(metrics.len(), 1);
6585        assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => {
6586            // RSSI velocity in [-2,-1) maps to bucket 9 and velocity in [2,3) maps to bucket 13.
6587            assert_eq!(buckets.len(), 2);
6588            assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 9, count: 1}));
6589            assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 13, count: 2}));
6590        });
6591        test_helper.clear_cobalt_events();
6592
6593        // Send another different RSSI velocity
6594        let rssi_velocity_3 = 3.0;
6595        test_helper
6596            .telemetry_sender
6597            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: rssi_velocity_3 });
6598        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6599
6600        // Check that the previously logged values are not logged again, and the new value is
6601        // logged.
6602        test_helper.drain_cobalt_events(&mut test_fut);
6603
6604        let metrics = test_helper.get_logged_metrics(metrics::RSSI_VELOCITY_METRIC_ID);
6605        assert_eq!(metrics.len(), 1);
6606        assert_eq!(
6607            metrics[0].payload,
6608            MetricEventPayload::Histogram(vec![fidl_fuchsia_metrics::HistogramBucket {
6609                index: 14,
6610                count: 1
6611            }])
6612        );
6613    }
6614
6615    #[fuchsia::test]
6616    fn test_log_rssi_histogram_bounds() {
6617        let (mut test_helper, mut test_fut) = setup_test();
6618
6619        // RSSI is only logged if in the connected state.
6620        test_helper.send_connected_event(random_bss_description!(Wpa2));
6621
6622        let ind_min = fidl_internal::SignalReportIndication {
6623            rssi_dbm: Some(-128),
6624            snr_db: Some(30),
6625            tx_rate_500kbps: Some(0),
6626            ..Default::default()
6627        };
6628        // 0 is the highest histogram bucket and 1 and above are in the overflow bucket.
6629        let ind_max = fidl_internal::SignalReportIndication {
6630            rssi_dbm: Some(0),
6631            snr_db: Some(30),
6632            tx_rate_500kbps: Some(0),
6633            ..Default::default()
6634        };
6635        let ind_overflow_1 = fidl_internal::SignalReportIndication {
6636            rssi_dbm: Some(1),
6637            snr_db: Some(30),
6638            tx_rate_500kbps: Some(0),
6639            ..Default::default()
6640        };
6641        let ind_overflow_2 = fidl_internal::SignalReportIndication {
6642            rssi_dbm: Some(127),
6643            snr_db: Some(30),
6644            tx_rate_500kbps: Some(0),
6645            ..Default::default()
6646        };
6647        // Send the telemetry events. -10 is the min velocity bucket and 10 is the max.
6648        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_min.clone() });
6649        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_min.clone() });
6650        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_min });
6651        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_max });
6652        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_overflow_1 });
6653        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_overflow_2 });
6654        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6655
6656        // Check that the min, max, underflow, and overflow buckets are used correctly.
6657        test_helper.drain_cobalt_events(&mut test_fut);
6658        // Check RSSI values
6659        let metrics = test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_METRIC_ID);
6660        assert_eq!(metrics.len(), 1);
6661        let buckets =
6662            assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => buckets);
6663        assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 1, count: 3 }));
6664        assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 129, count: 1 }));
6665        assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 130, count: 2 }));
6666    }
6667
6668    #[fuchsia::test]
6669    fn test_log_rssi_velocity_histogram_bounds() {
6670        let (mut test_helper, mut test_fut) = setup_test();
6671
6672        // RSSI velocity is only logged if in the connected state.
6673        test_helper.send_connected_event(random_bss_description!(Wpa2));
6674
6675        // Send the telemetry events. -10 is the min velocity bucket and 10 is the max.
6676        test_helper
6677            .telemetry_sender
6678            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: -11.0 });
6679        test_helper
6680            .telemetry_sender
6681            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: -15.0 });
6682        test_helper
6683            .telemetry_sender
6684            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: 11.0 });
6685        test_helper
6686            .telemetry_sender
6687            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: 20.0 });
6688        test_helper
6689            .telemetry_sender
6690            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: -10.0 });
6691        test_helper
6692            .telemetry_sender
6693            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: 10.0 });
6694        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6695
6696        // Check that the min, max, underflow, and overflow buckets are used correctly.
6697        test_helper.drain_cobalt_events(&mut test_fut);
6698
6699        // Check RSSI velocity values
6700        let metrics = test_helper.get_logged_metrics(metrics::RSSI_VELOCITY_METRIC_ID);
6701        assert_eq!(metrics.len(), 1);
6702        let buckets =
6703            assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => buckets);
6704        // RSSI velocity below -10 maps to underflow bucket, and 11 or above maps to overflow.
6705        assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 1, count: 1 }));
6706        assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 21, count: 1 }));
6707        assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 0, count: 2 }));
6708        assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 22, count: 2 }));
6709    }
6710
6711    #[fuchsia::test]
6712    fn test_log_short_duration_connection_metrics() {
6713        let (mut test_helper, mut test_fut) = setup_test();
6714        let now = fasync::MonotonicInstant::now();
6715        test_helper.send_connected_event(random_bss_description!(Wpa2));
6716        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6717
6718        let channel = generate_random_channel();
6719        let ap_state = random_bss_description!(Wpa2, channel: channel).into();
6720        let mut signals = HistoricalList::new();
6721        signals.add(client::types::TimestampedSignal {
6722            signal: client::types::Signal { rssi_dbm: -30, snr_db: 60 },
6723            time: now,
6724        });
6725        signals.add(client::types::TimestampedSignal {
6726            signal: client::types::Signal { rssi_dbm: -30, snr_db: 60 },
6727            time: now,
6728        });
6729        // Log disconnect with reason FidlConnectRequest during short duration
6730        let info = DisconnectInfo {
6731            connected_duration: METRICS_SHORT_CONNECT_DURATION
6732                - zx::MonotonicDuration::from_seconds(1),
6733            disconnect_source: fidl_sme::DisconnectSource::User(
6734                fidl_sme::UserDisconnectReason::FidlConnectRequest,
6735            ),
6736            ap_state,
6737            signals,
6738            ..fake_disconnect_info()
6739        };
6740        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6741            track_subsequent_downtime: true,
6742            info: Some(info.clone()),
6743        });
6744
6745        test_helper.send_connected_event(random_bss_description!(Wpa2));
6746        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6747
6748        // Log disconnect with reason NetworkUnsaved during short duration
6749        let info = DisconnectInfo {
6750            disconnect_source: fidl_sme::DisconnectSource::User(
6751                fidl_sme::UserDisconnectReason::NetworkUnsaved,
6752            ),
6753            ..info
6754        };
6755        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6756            track_subsequent_downtime: true,
6757            info: Some(info.clone()),
6758        });
6759
6760        test_helper.send_connected_event(random_bss_description!(Wpa2));
6761        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6762
6763        // Log disconnect with reason NetworkUnsaved during longer duration connection
6764        let info = DisconnectInfo {
6765            connected_duration: METRICS_SHORT_CONNECT_DURATION
6766                + zx::MonotonicDuration::from_seconds(1),
6767            ..info
6768        };
6769        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6770            track_subsequent_downtime: true,
6771            info: Some(info.clone()),
6772        });
6773
6774        test_helper.drain_cobalt_events(&mut test_fut);
6775
6776        let logged_metrics = test_helper.get_logged_metrics(
6777            metrics::POLICY_FIDL_CONNECTION_ATTEMPTS_DURING_SHORT_CONNECTION_METRIC_ID,
6778        );
6779        assert_eq!(logged_metrics.len(), 2);
6780
6781        let logged_metrics = test_helper.get_logged_metrics(
6782            metrics::POLICY_FIDL_CONNECTION_ATTEMPTS_DURING_SHORT_CONNECTION_DETAILED_METRIC_ID,
6783        );
6784        assert_eq!(logged_metrics.len(), 2);
6785        assert_eq!(logged_metrics[0].event_codes, vec![info.previous_connect_reason as u32]);
6786
6787        let logged_metrics =
6788            test_helper.get_logged_metrics(metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID);
6789        assert_eq!(logged_metrics.len(), 2);
6790        assert_eq!(
6791            logged_metrics[0].event_codes,
6792            vec![metrics::ConnectionScoreAverageMetricDimensionDuration::ShortDuration as u32]
6793        );
6794        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(100));
6795    }
6796
6797    #[fuchsia::test]
6798    fn test_log_disconnect_cobalt_metrics() {
6799        let (mut test_helper, mut test_fut) = setup_test();
6800        test_helper.advance_by(zx::MonotonicDuration::from_hours(3), test_fut.as_mut());
6801        test_helper.send_connected_event(random_bss_description!(Wpa2));
6802        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6803
6804        test_helper.advance_by(zx::MonotonicDuration::from_hours(5), test_fut.as_mut());
6805
6806        let primary_channel = 8;
6807        let channel = Channel::new(primary_channel, Bandwidth::Cbw20, TwoGhz);
6808        let ap_state: client::types::ApState =
6809            random_bss_description!(Wpa2, channel: channel).into();
6810        let info = DisconnectInfo {
6811            connected_duration: zx::MonotonicDuration::from_hours(5),
6812            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
6813                reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
6814                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
6815            }),
6816            ap_state: ap_state.clone(),
6817            ..fake_disconnect_info()
6818        };
6819        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6820            track_subsequent_downtime: true,
6821            info: Some(info),
6822        });
6823        test_helper.drain_cobalt_events(&mut test_fut);
6824
6825        let policy_disconnection_reasons =
6826            test_helper.get_logged_metrics(metrics::POLICY_DISCONNECTION_MIGRATED_METRIC_ID);
6827        assert_eq!(policy_disconnection_reasons.len(), 1);
6828        assert_eq!(policy_disconnection_reasons[0].payload, MetricEventPayload::Count(1));
6829        assert_eq!(
6830            policy_disconnection_reasons[0].event_codes,
6831            vec![client::types::DisconnectReason::DisconnectDetectedFromSme as u32]
6832        );
6833
6834        let breakdowns_by_device_uptime = test_helper
6835            .get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_DEVICE_UPTIME_METRIC_ID);
6836        assert_eq!(breakdowns_by_device_uptime.len(), 1);
6837        assert_eq!(breakdowns_by_device_uptime[0].event_codes, vec![
6838            metrics::DisconnectBreakdownByDeviceUptimeMetricDimensionDeviceUptime::LessThan12Hours as u32,
6839        ]);
6840        assert_eq!(breakdowns_by_device_uptime[0].payload, MetricEventPayload::Count(1));
6841
6842        let breakdowns_by_connected_duration = test_helper
6843            .get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_CONNECTED_DURATION_METRIC_ID);
6844        assert_eq!(breakdowns_by_connected_duration.len(), 1);
6845        assert_eq!(breakdowns_by_connected_duration[0].event_codes, vec![
6846            metrics::DisconnectBreakdownByConnectedDurationMetricDimensionConnectedDuration::LessThan6Hours as u32,
6847        ]);
6848        assert_eq!(breakdowns_by_connected_duration[0].payload, MetricEventPayload::Count(1));
6849
6850        let breakdowns_by_reason =
6851            test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID);
6852        assert_eq!(breakdowns_by_reason.len(), 1);
6853        assert_eq!(
6854            breakdowns_by_reason[0].event_codes,
6855            vec![3u32, metrics::ConnectivityWlanMetricDimensionDisconnectSource::Mlme as u32,]
6856        );
6857        assert_eq!(breakdowns_by_reason[0].payload, MetricEventPayload::Count(1));
6858
6859        let breakdowns_by_channel = test_helper
6860            .get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID);
6861        assert_eq!(breakdowns_by_channel.len(), 1);
6862        assert_eq!(breakdowns_by_channel[0].event_codes, vec![channel.primary as u32]);
6863        assert_eq!(breakdowns_by_channel[0].payload, MetricEventPayload::Count(1));
6864
6865        let breakdowns_by_channel_band =
6866            test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID);
6867        assert_eq!(breakdowns_by_channel_band.len(), 1);
6868        assert_eq!(
6869            breakdowns_by_channel_band[0].event_codes,
6870            vec![
6871                metrics::DisconnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz
6872                    as u32
6873            ]
6874        );
6875        assert_eq!(breakdowns_by_channel_band[0].payload, MetricEventPayload::Count(1));
6876
6877        let breakdowns_by_is_multi_bss =
6878            test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID);
6879        assert_eq!(breakdowns_by_is_multi_bss.len(), 1);
6880        assert_eq!(
6881            breakdowns_by_is_multi_bss[0].event_codes,
6882            vec![metrics::DisconnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes as u32]
6883        );
6884        assert_eq!(breakdowns_by_is_multi_bss[0].payload, MetricEventPayload::Count(1));
6885
6886        let breakdowns_by_security_type = test_helper
6887            .get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID);
6888        assert_eq!(breakdowns_by_security_type.len(), 1);
6889        assert_eq!(
6890            breakdowns_by_security_type[0].event_codes,
6891            vec![
6892                metrics::DisconnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal
6893                    as u32
6894            ]
6895        );
6896        assert_eq!(breakdowns_by_security_type[0].payload, MetricEventPayload::Count(1));
6897
6898        // Connected duration should be logged for overall disconnect metric and for non-roam
6899        // metric.
6900        let connected_duration_before_disconnect =
6901            test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID);
6902        assert_eq!(connected_duration_before_disconnect.len(), 1);
6903        assert_eq!(
6904            connected_duration_before_disconnect[0].payload,
6905            MetricEventPayload::IntegerValue(300)
6906        );
6907        let connected_duration_before_non_roam_disconnect = test_helper
6908            .get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_NON_ROAM_DISCONNECT_METRIC_ID);
6909        assert_eq!(connected_duration_before_non_roam_disconnect.len(), 1);
6910        assert_eq!(
6911            connected_duration_before_non_roam_disconnect[0].payload,
6912            MetricEventPayload::IntegerValue(300)
6913        );
6914        let connected_duration_before_roam_attempt = test_helper.get_logged_metrics(
6915            metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
6916        );
6917        assert_eq!(connected_duration_before_roam_attempt.len(), 0);
6918
6919        // Disconnect count should be logged for overall disconnect metric and for non-roam
6920        // metric.
6921        let network_disconnect_counts =
6922            test_helper.get_logged_metrics(metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID);
6923        assert_eq!(network_disconnect_counts.len(), 1);
6924        assert_eq!(network_disconnect_counts[0].payload, MetricEventPayload::Count(1));
6925
6926        let non_roam_disconnect_counts =
6927            test_helper.get_logged_metrics(metrics::NON_ROAM_DISCONNECT_COUNTS_METRIC_ID);
6928        assert_eq!(non_roam_disconnect_counts.len(), 1);
6929        assert_eq!(non_roam_disconnect_counts[0].payload, MetricEventPayload::Count(1));
6930
6931        let roam_disconnect_counts =
6932            test_helper.get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_METRIC_ID);
6933        assert!(roam_disconnect_counts.is_empty());
6934
6935        // Clear events.
6936        test_helper.clear_cobalt_events();
6937
6938        // Advance and get state back to connected.
6939        test_helper.advance_by(zx::MonotonicDuration::from_minutes(1), test_fut.as_mut());
6940        test_helper.send_connected_event(random_bss_description!(Wpa2));
6941        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6942
6943        test_helper.advance_by(zx::MonotonicDuration::from_hours(6), test_fut.as_mut());
6944
6945        // Send a disconnect count with roam cause.
6946        let info = DisconnectInfo {
6947            connected_duration: zx::MonotonicDuration::from_hours(6),
6948            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
6949                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
6950                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
6951            }),
6952            ap_state,
6953            ..fake_disconnect_info()
6954        };
6955        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6956            track_subsequent_downtime: true,
6957            info: Some(info),
6958        });
6959        test_helper.drain_cobalt_events(&mut test_fut);
6960
6961        // Connected duration should be logged for overall disconnect metric, but not for non-roam
6962        // metric.
6963        let connected_duration_before_disconnect =
6964            test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID);
6965        assert_eq!(connected_duration_before_disconnect.len(), 1);
6966        assert_eq!(
6967            connected_duration_before_disconnect[0].payload,
6968            MetricEventPayload::IntegerValue(360)
6969        );
6970
6971        let connected_duration_before_non_roam_disconnect = test_helper
6972            .get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_NON_ROAM_DISCONNECT_METRIC_ID);
6973        assert!(connected_duration_before_non_roam_disconnect.is_empty());
6974
6975        // Connected duration before roam attempt should also not be logged, despite the roam
6976        // disconnect source, because we log that in the roam result event where we can distinguish
6977        // successful roams from failed roams.
6978        let connected_duration_before_roam_attempt = test_helper.get_logged_metrics(
6979            metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
6980        );
6981        assert!(connected_duration_before_roam_attempt.is_empty());
6982
6983        // Disconnect count should be logged for overall disconnect metric, but not for non-roam
6984        // metric.
6985        let network_disconnect_counts =
6986            test_helper.get_logged_metrics(metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID);
6987        assert_eq!(network_disconnect_counts.len(), 1);
6988        assert_eq!(network_disconnect_counts[0].payload, MetricEventPayload::Count(1));
6989
6990        let non_roam_disconnect_counts =
6991            test_helper.get_logged_metrics(metrics::NON_ROAM_DISCONNECT_COUNTS_METRIC_ID);
6992        assert!(non_roam_disconnect_counts.is_empty());
6993
6994        // Roam disconnect count should not be logged, because we log that in the roam result event.
6995        let roam_disconnect_counts =
6996            test_helper.get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_METRIC_ID);
6997        assert!(roam_disconnect_counts.is_empty());
6998    }
6999
7000    #[fuchsia::test]
7001    fn test_log_user_disconnect_cobalt_metrics() {
7002        let (mut test_helper, mut test_fut) = setup_test();
7003        test_helper.advance_by(zx::MonotonicDuration::from_hours(3), test_fut.as_mut());
7004        test_helper.send_connected_event(random_bss_description!(Wpa2));
7005        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7006
7007        const DUR_MIN: i64 = 250;
7008        test_helper.advance_by(zx::MonotonicDuration::from_minutes(DUR_MIN), test_fut.as_mut());
7009
7010        // Send a disconnect event.
7011        let info = DisconnectInfo {
7012            connected_duration: zx::MonotonicDuration::from_minutes(DUR_MIN),
7013            disconnect_source: fidl_sme::DisconnectSource::User(
7014                fidl_sme::UserDisconnectReason::FidlConnectRequest,
7015            ),
7016            ..fake_disconnect_info()
7017        };
7018        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7019            track_subsequent_downtime: true,
7020            info: Some(info),
7021        });
7022        test_helper.drain_cobalt_events(&mut test_fut);
7023
7024        // Check that nothing was logged for roaming disconnects.
7025        let roam_connected_duration = test_helper.get_logged_metrics(
7026            metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
7027        );
7028        assert_eq!(roam_connected_duration.len(), 0);
7029
7030        // Check that a non_roam disconnect was logged
7031        let non_roam_connected_duration = test_helper
7032            .get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_NON_ROAM_DISCONNECT_METRIC_ID);
7033        assert_eq!(non_roam_connected_duration.len(), 1);
7034
7035        let roam_disconnect_counts =
7036            test_helper.get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_METRIC_ID);
7037        assert!(roam_disconnect_counts.is_empty());
7038
7039        let non_roam_disconnect_counts =
7040            test_helper.get_logged_metrics(metrics::NON_ROAM_DISCONNECT_COUNTS_METRIC_ID);
7041        assert!(!non_roam_disconnect_counts.is_empty());
7042
7043        // Check that a connected duration and a count were logged for overall disconnects.
7044        let total_connected_duration =
7045            test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID);
7046        assert_eq!(total_connected_duration.len(), 1);
7047        assert_eq!(total_connected_duration[0].payload, MetricEventPayload::IntegerValue(DUR_MIN));
7048
7049        let total_disconnect_counts =
7050            test_helper.get_logged_metrics(metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID);
7051        assert_eq!(total_disconnect_counts.len(), 1);
7052        assert_eq!(total_disconnect_counts[0].payload, MetricEventPayload::Count(1));
7053    }
7054
7055    #[fuchsia::test]
7056    fn test_log_saved_networks_count() {
7057        let (mut test_helper, mut test_fut) = setup_test();
7058
7059        let event = TelemetryEvent::SavedNetworkCount {
7060            saved_network_count: 4,
7061            config_count_per_saved_network: vec![1, 1],
7062        };
7063        test_helper.telemetry_sender.send(event);
7064        test_helper.drain_cobalt_events(&mut test_fut);
7065
7066        let saved_networks_count =
7067            test_helper.get_logged_metrics(metrics::SAVED_NETWORKS_MIGRATED_METRIC_ID);
7068        assert_eq!(saved_networks_count.len(), 1);
7069        assert_eq!(
7070            saved_networks_count[0].event_codes,
7071            vec![metrics::SavedNetworksMigratedMetricDimensionSavedNetworks::TwoToFour as u32]
7072        );
7073
7074        let config_count = test_helper
7075            .get_logged_metrics(metrics::SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_MIGRATED_METRIC_ID);
7076        assert_eq!(config_count.len(), 2);
7077        assert_eq!(
7078            config_count[0].event_codes,
7079            vec![metrics::SavedConfigurationsForSavedNetworkMigratedMetricDimensionSavedConfigurations::One as u32]
7080        );
7081        assert_eq!(
7082            config_count[1].event_codes,
7083            vec![metrics::SavedConfigurationsForSavedNetworkMigratedMetricDimensionSavedConfigurations::One as u32]
7084        );
7085    }
7086
7087    #[fuchsia::test]
7088    fn test_log_network_selection_scan_interval() {
7089        let (mut test_helper, mut test_fut) = setup_test();
7090
7091        let duration = zx::MonotonicDuration::from_seconds(rand::random_range(0..100));
7092
7093        let event = TelemetryEvent::NetworkSelectionScanInterval { time_since_last_scan: duration };
7094        test_helper.telemetry_sender.send(event);
7095        test_helper.drain_cobalt_events(&mut test_fut);
7096
7097        let last_scan_age = test_helper
7098            .get_logged_metrics(metrics::LAST_SCAN_AGE_WHEN_SCAN_REQUESTED_MIGRATED_METRIC_ID);
7099        assert_eq!(last_scan_age.len(), 1);
7100        assert_eq!(
7101            last_scan_age[0].payload,
7102            fidl_fuchsia_metrics::MetricEventPayload::IntegerValue(duration.into_micros())
7103        );
7104    }
7105
7106    #[fuchsia::test]
7107    fn test_log_connection_selection_scan_results() {
7108        let (mut test_helper, mut test_fut) = setup_test();
7109
7110        let event = TelemetryEvent::ConnectionSelectionScanResults {
7111            saved_network_count: 4,
7112            saved_network_count_found_by_active_scan: 1,
7113            bss_count_per_saved_network: vec![10, 10],
7114        };
7115        test_helper.telemetry_sender.send(event);
7116        test_helper.drain_cobalt_events(&mut test_fut);
7117
7118        let saved_networks_count =
7119            test_helper.get_logged_metrics(metrics::SCAN_RESULTS_RECEIVED_MIGRATED_METRIC_ID);
7120        assert_eq!(saved_networks_count.len(), 1);
7121        assert_eq!(
7122            saved_networks_count[0].event_codes,
7123            vec![
7124                metrics::ScanResultsReceivedMigratedMetricDimensionSavedNetworksCount::TwoToFour
7125                    as u32
7126            ]
7127        );
7128
7129        let active_scanned_network = test_helper.get_logged_metrics(
7130            metrics::SAVED_NETWORK_IN_SCAN_RESULT_WITH_ACTIVE_SCAN_MIGRATED_METRIC_ID,
7131        );
7132        assert_eq!(active_scanned_network.len(), 1);
7133        assert_eq!(
7134            active_scanned_network[0].event_codes,
7135            vec![metrics::SavedNetworkInScanResultWithActiveScanMigratedMetricDimensionActiveScanSsidsObserved::One as u32]
7136        );
7137
7138        let bss_count = test_helper
7139            .get_logged_metrics(metrics::SAVED_NETWORK_IN_SCAN_RESULT_MIGRATED_METRIC_ID);
7140        assert_eq!(bss_count.len(), 2);
7141        assert_eq!(
7142            bss_count[0].event_codes,
7143            vec![
7144                metrics::SavedNetworkInScanResultMigratedMetricDimensionBssCount::FiveToTen as u32
7145            ]
7146        );
7147        assert_eq!(
7148            bss_count[1].event_codes,
7149            vec![
7150                metrics::SavedNetworkInScanResultMigratedMetricDimensionBssCount::FiveToTen as u32
7151            ]
7152        );
7153    }
7154
7155    #[fuchsia::test]
7156    fn test_log_establish_connection_cobalt_metrics() {
7157        let (mut test_helper, mut test_fut) = setup_test();
7158
7159        let primary_channel = 8;
7160        let channel = Channel::new(primary_channel, Bandwidth::Cbw20, TwoGhz);
7161        let ap_state = random_bss_description!(Wpa2,
7162            bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
7163            channel: channel,
7164            rssi_dbm: -50,
7165            snr_db: 25,
7166        )
7167        .into();
7168        let event = TelemetryEvent::ConnectResult {
7169            iface_id: IFACE_ID,
7170            policy_connect_reason: Some(client::types::ConnectReason::FidlConnectRequest),
7171            result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
7172            multiple_bss_candidates: true,
7173            ap_state,
7174            network_is_likely_hidden: true,
7175        };
7176        test_helper.telemetry_sender.send(event);
7177        test_helper.drain_cobalt_events(&mut test_fut);
7178
7179        let policy_connect_reasons =
7180            test_helper.get_logged_metrics(metrics::POLICY_CONNECTION_ATTEMPT_MIGRATED_METRIC_ID);
7181        assert_eq!(policy_connect_reasons.len(), 1);
7182        assert_eq!(
7183            policy_connect_reasons[0].event_codes,
7184            vec![client::types::ConnectReason::FidlConnectRequest as u32]
7185        );
7186        assert_eq!(policy_connect_reasons[0].payload, MetricEventPayload::Count(1));
7187
7188        let breakdowns_by_user_wait_time = test_helper
7189            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7190        // TelemetryEvent::StartEstablishConnection is never sent, so connect start time is never
7191        // tracked, hence this metric is not logged.
7192        assert_eq!(breakdowns_by_user_wait_time.len(), 0);
7193
7194        let breakdowns_by_is_multi_bss = test_helper
7195            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID);
7196        assert_eq!(breakdowns_by_is_multi_bss.len(), 1);
7197        assert_eq!(
7198            breakdowns_by_is_multi_bss[0].event_codes,
7199            vec![
7200                metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes
7201                    as u32
7202            ]
7203        );
7204        assert_eq!(breakdowns_by_is_multi_bss[0].payload, MetricEventPayload::Count(1));
7205
7206        let breakdowns_by_security_type = test_helper
7207            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID);
7208        assert_eq!(breakdowns_by_security_type.len(), 1);
7209        assert_eq!(
7210            breakdowns_by_security_type[0].event_codes,
7211            vec![
7212                metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal
7213                    as u32
7214            ]
7215        );
7216        assert_eq!(breakdowns_by_security_type[0].payload, MetricEventPayload::Count(1));
7217
7218        let breakdowns_by_channel = test_helper
7219            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID);
7220        assert_eq!(breakdowns_by_channel.len(), 1);
7221        assert_eq!(breakdowns_by_channel[0].event_codes, vec![primary_channel as u32]);
7222        assert_eq!(breakdowns_by_channel[0].payload, MetricEventPayload::Count(1));
7223
7224        let breakdowns_by_channel_band = test_helper
7225            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID);
7226        assert_eq!(breakdowns_by_channel_band.len(), 1);
7227        assert_eq!(breakdowns_by_channel_band[0].event_codes, vec![
7228            metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz as u32
7229        ]);
7230        assert_eq!(breakdowns_by_channel_band[0].payload, MetricEventPayload::Count(1));
7231
7232        let fidl_connect_count =
7233            test_helper.get_logged_metrics(metrics::POLICY_CONNECTION_ATTEMPTS_METRIC_ID);
7234        assert_eq!(fidl_connect_count.len(), 1);
7235        assert_eq!(fidl_connect_count[0].payload, MetricEventPayload::Count(1));
7236
7237        let network_is_likely_hidden =
7238            test_helper.get_logged_metrics(metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID);
7239        assert_eq!(network_is_likely_hidden.len(), 1);
7240        assert_eq!(network_is_likely_hidden[0].payload, MetricEventPayload::Count(1));
7241    }
7242
7243    #[fuchsia::test]
7244    fn test_log_establish_connection_status_code_cobalt_metrics_normal_device() {
7245        let (mut test_helper, mut test_fut) = setup_test();
7246        for _ in 0..3 {
7247            let event = TelemetryEvent::ConnectResult {
7248                iface_id: IFACE_ID,
7249                policy_connect_reason: Some(
7250                    client::types::ConnectReason::RetryAfterFailedConnectAttempt,
7251                ),
7252                result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
7253                multiple_bss_candidates: true,
7254                ap_state: random_bss_description!(Wpa1).into(),
7255                network_is_likely_hidden: true,
7256            };
7257            test_helper.telemetry_sender.send(event);
7258        }
7259        test_helper.send_connected_event(random_bss_description!(Wpa2));
7260        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
7261
7262        let status_codes = test_helper.get_logged_metrics(
7263            metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7264        );
7265        assert_eq!(status_codes.len(), 2);
7266        assert_eq_cobalt_events(
7267            status_codes,
7268            vec![
7269                MetricEvent {
7270                    metric_id:
7271                        metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7272                    event_codes: vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32],
7273                    payload: MetricEventPayload::Count(1),
7274                },
7275                MetricEvent {
7276                    metric_id:
7277                        metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7278                    event_codes: vec![
7279                        fidl_ieee80211::StatusCode::RefusedReasonUnspecified.into_primitive()
7280                            as u32,
7281                    ],
7282                    payload: MetricEventPayload::Count(3),
7283                },
7284            ],
7285        );
7286    }
7287
7288    #[fuchsia::test]
7289    fn test_log_establish_connection_status_code_cobalt_metrics_bad_device() {
7290        let (mut test_helper, mut test_fut) = setup_test();
7291        for _ in 0..10 {
7292            let event = TelemetryEvent::ConnectResult {
7293                iface_id: IFACE_ID,
7294                policy_connect_reason: Some(
7295                    client::types::ConnectReason::RetryAfterFailedConnectAttempt,
7296                ),
7297                result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
7298                multiple_bss_candidates: true,
7299                ap_state: random_bss_description!(Wpa1).into(),
7300                network_is_likely_hidden: true,
7301            };
7302            test_helper.telemetry_sender.send(event);
7303        }
7304        test_helper.send_connected_event(random_bss_description!(Wpa2));
7305        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
7306
7307        let status_codes = test_helper.get_logged_metrics(
7308            metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7309        );
7310        assert_eq!(status_codes.len(), 2);
7311        assert_eq_cobalt_events(
7312            status_codes,
7313            vec![
7314                MetricEvent {
7315                    metric_id:
7316                        metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7317                    event_codes: vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32],
7318                    payload: MetricEventPayload::Count(1),
7319                },
7320                MetricEvent {
7321                    metric_id:
7322                        metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7323                    event_codes: vec![
7324                        fidl_ieee80211::StatusCode::RefusedReasonUnspecified.into_primitive()
7325                            as u32,
7326                    ],
7327                    payload: MetricEventPayload::Count(10),
7328                },
7329            ],
7330        );
7331    }
7332
7333    #[fuchsia::test]
7334    fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_no_reset() {
7335        let (mut test_helper, mut test_fut) = setup_test();
7336
7337        test_helper
7338            .telemetry_sender
7339            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7340        test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7341        test_helper
7342            .telemetry_sender
7343            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7344        test_helper.advance_by(zx::MonotonicDuration::from_seconds(4), test_fut.as_mut());
7345        test_helper.send_connected_event(random_bss_description!(Wpa2));
7346        test_helper.drain_cobalt_events(&mut test_fut);
7347
7348        let breakdowns_by_user_wait_time = test_helper
7349            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7350        assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7351        assert_eq!(
7352            breakdowns_by_user_wait_time[0].event_codes,
7353            // Both the 2 seconds and 4 seconds since the first StartEstablishConnection
7354            // should be counted.
7355            vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan8Seconds as u32]
7356        );
7357    }
7358
7359    #[fuchsia::test]
7360    fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_with_reset() {
7361        let (mut test_helper, mut test_fut) = setup_test();
7362
7363        test_helper
7364            .telemetry_sender
7365            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7366        test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7367        test_helper
7368            .telemetry_sender
7369            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: true });
7370        test_helper.advance_by(zx::MonotonicDuration::from_seconds(4), test_fut.as_mut());
7371        test_helper.send_connected_event(random_bss_description!(Wpa2));
7372        test_helper.drain_cobalt_events(&mut test_fut);
7373
7374        let breakdowns_by_user_wait_time = test_helper
7375            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7376        assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7377        assert_eq!(
7378            breakdowns_by_user_wait_time[0].event_codes,
7379            // Only the 4 seconds after the last StartEstablishConnection should be counted.
7380            vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan5Seconds as u32]
7381        );
7382    }
7383
7384    #[fuchsia::test]
7385    fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_with_clear() {
7386        let (mut test_helper, mut test_fut) = setup_test();
7387
7388        test_helper
7389            .telemetry_sender
7390            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7391        test_helper.advance_by(zx::MonotonicDuration::from_seconds(10), test_fut.as_mut());
7392        test_helper.telemetry_sender.send(TelemetryEvent::ClearEstablishConnectionStartTime);
7393
7394        test_helper.advance_by(zx::MonotonicDuration::from_seconds(30), test_fut.as_mut());
7395
7396        test_helper
7397            .telemetry_sender
7398            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7399        test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7400        test_helper.send_connected_event(random_bss_description!(Wpa2));
7401        test_helper.drain_cobalt_events(&mut test_fut);
7402
7403        let breakdowns_by_user_wait_time = test_helper
7404            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7405        assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7406        assert_eq!(
7407            breakdowns_by_user_wait_time[0].event_codes,
7408            // Only the 2 seconds after the last StartEstablishConnection should be counted.
7409            vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan3Seconds as u32]
7410        );
7411    }
7412
7413    #[test_case(
7414        (true, random_bss_description!(Wpa2)),
7415        (false, random_bss_description!(Wpa2)),
7416        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
7417        metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes as u32,
7418        metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::No as u32;
7419        "breakdown_by_is_multi_bss"
7420    )]
7421    #[test_case(
7422        (false, random_bss_description!(Wpa1)),
7423        (false, random_bss_description!(Wpa2)),
7424        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
7425        metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa1 as u32,
7426        metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal as u32;
7427        "breakdown_by_security_type"
7428    )]
7429    #[test_case(
7430        (false, random_bss_description!(Wpa2, channel: Channel::new(6, Bandwidth::Cbw20, TwoGhz))),
7431        (false, random_bss_description!(Wpa2, channel: Channel::new(157, Bandwidth::Cbw40, FiveGhz))),
7432        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
7433        6,
7434        157;
7435        "breakdown_by_primary_channel"
7436    )]
7437    #[test_case(
7438        (false, random_bss_description!(Wpa2, channel: Channel::new(6, Bandwidth::Cbw20, TwoGhz))),
7439        (false, random_bss_description!(Wpa2, channel: Channel::new(157, Bandwidth::Cbw40, FiveGhz))),
7440        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
7441        metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz as u32,
7442        metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band5Ghz as u32;
7443        "breakdown_by_channel_band"
7444    )]
7445    #[test_case(
7446        (false, random_bss_description!(Wpa2, rssi_dbm: -79)),
7447        (false, random_bss_description!(Wpa2, rssi_dbm: -40)),
7448        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_RSSI_BUCKET_METRIC_ID,
7449        metrics::ConnectivityWlanMetricDimensionRssiBucket::From79To77 as u32,
7450        metrics::ConnectivityWlanMetricDimensionRssiBucket::From50To35 as u32;
7451        "breakdown_by_rssi_bucket"
7452    )]
7453    #[test_case(
7454        (false, random_bss_description!(Wpa2, snr_db: 11)),
7455        (false, random_bss_description!(Wpa2, snr_db: 35)),
7456        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SNR_BUCKET_METRIC_ID,
7457        metrics::ConnectivityWlanMetricDimensionSnrBucket::From11To15 as u32,
7458        metrics::ConnectivityWlanMetricDimensionSnrBucket::From26To40 as u32;
7459        "breakdown_by_snr_bucket"
7460    )]
7461    #[fuchsia::test(add_test_attr = false)]
7462    fn test_log_daily_connect_success_rate_breakdown_cobalt_metrics(
7463        first_connect_result_params: (bool, BssDescription),
7464        second_connect_result_params: (bool, BssDescription),
7465        metric_id: u32,
7466        event_code_1: u32,
7467        event_code_2: u32,
7468    ) {
7469        let (mut test_helper, mut test_fut) = setup_test();
7470
7471        for i in 0..3 {
7472            let code = if i == 0 {
7473                fidl_ieee80211::StatusCode::Success
7474            } else {
7475                fidl_ieee80211::StatusCode::RefusedReasonUnspecified
7476            };
7477            let event = TelemetryEvent::ConnectResult {
7478                iface_id: IFACE_ID,
7479                policy_connect_reason: Some(
7480                    client::types::ConnectReason::RetryAfterFailedConnectAttempt,
7481                ),
7482                result: fake_connect_result(code),
7483                multiple_bss_candidates: first_connect_result_params.0,
7484                ap_state: first_connect_result_params.1.clone().into(),
7485                network_is_likely_hidden: true,
7486            };
7487            test_helper.telemetry_sender.send(event);
7488        }
7489        for i in 0..2 {
7490            let code = if i == 0 {
7491                fidl_ieee80211::StatusCode::Success
7492            } else {
7493                fidl_ieee80211::StatusCode::RefusedReasonUnspecified
7494            };
7495            let event = TelemetryEvent::ConnectResult {
7496                iface_id: IFACE_ID,
7497                policy_connect_reason: Some(
7498                    client::types::ConnectReason::RetryAfterFailedConnectAttempt,
7499                ),
7500                result: fake_connect_result(code),
7501                multiple_bss_candidates: second_connect_result_params.0,
7502                ap_state: second_connect_result_params.1.clone().into(),
7503                network_is_likely_hidden: true,
7504            };
7505            test_helper.telemetry_sender.send(event);
7506        }
7507
7508        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
7509
7510        let metrics = test_helper.get_logged_metrics(metric_id);
7511        assert_eq!(metrics.len(), 2);
7512        assert_eq_cobalt_events(
7513            metrics,
7514            vec![
7515                MetricEvent {
7516                    metric_id,
7517                    event_codes: vec![event_code_1],
7518                    payload: MetricEventPayload::IntegerValue(3333), // 1/3 = 33.33%
7519                },
7520                MetricEvent {
7521                    metric_id,
7522                    event_codes: vec![event_code_2],
7523                    payload: MetricEventPayload::IntegerValue(5000), // 1/2 = 50.00%
7524                },
7525            ],
7526        );
7527    }
7528
7529    #[fuchsia::test]
7530    fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_while_connected() {
7531        let (mut test_helper, mut test_fut) = setup_test();
7532        test_helper.send_connected_event(random_bss_description!(Wpa2));
7533        test_helper.drain_cobalt_events(&mut test_fut);
7534        test_helper.cobalt_events.clear();
7535
7536        test_helper
7537            .telemetry_sender
7538            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: true });
7539        test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7540        let info = fake_disconnect_info();
7541        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7542            track_subsequent_downtime: false,
7543            info: Some(info),
7544        });
7545        test_helper.advance_by(zx::MonotonicDuration::from_seconds(4), test_fut.as_mut());
7546        test_helper.send_connected_event(random_bss_description!(Wpa2));
7547        test_helper.drain_cobalt_events(&mut test_fut);
7548
7549        let breakdowns_by_user_wait_time = test_helper
7550            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7551        assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7552        assert_eq!(
7553            breakdowns_by_user_wait_time[0].event_codes,
7554            // Both the 2 seconds and 4 seconds since the first StartEstablishConnection
7555            // should be counted.
7556            vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan8Seconds as u32]
7557        );
7558    }
7559
7560    #[fuchsia::test]
7561    fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_with_clear_while_connected()
7562     {
7563        let (mut test_helper, mut test_fut) = setup_test();
7564        test_helper.send_connected_event(random_bss_description!(Wpa2));
7565        test_helper.drain_cobalt_events(&mut test_fut);
7566        test_helper.cobalt_events.clear();
7567
7568        test_helper
7569            .telemetry_sender
7570            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: true });
7571        test_helper.telemetry_sender.send(TelemetryEvent::ClearEstablishConnectionStartTime);
7572        let info = fake_disconnect_info();
7573        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7574            track_subsequent_downtime: false,
7575            info: Some(info),
7576        });
7577        test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7578        test_helper
7579            .telemetry_sender
7580            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7581        test_helper.advance_by(zx::MonotonicDuration::from_seconds(4), test_fut.as_mut());
7582        test_helper.send_connected_event(random_bss_description!(Wpa2));
7583        test_helper.drain_cobalt_events(&mut test_fut);
7584
7585        let breakdowns_by_user_wait_time = test_helper
7586            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7587        assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7588        assert_eq!(
7589            breakdowns_by_user_wait_time[0].event_codes,
7590            // Only the 4 seconds after the last StartEstablishConnection should be counted.
7591            vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan5Seconds as u32]
7592        );
7593    }
7594
7595    #[fuchsia::test]
7596    fn test_log_establish_connection_cobalt_metrics_user_wait_time_logged_for_sme_reconnecting() {
7597        let (mut test_helper, mut test_fut) = setup_test();
7598        test_helper.send_connected_event(random_bss_description!(Wpa2));
7599        test_helper.drain_cobalt_events(&mut test_fut);
7600        test_helper.cobalt_events.clear();
7601
7602        let info = DisconnectInfo { is_sme_reconnecting: true, ..fake_disconnect_info() };
7603        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7604            track_subsequent_downtime: false,
7605            info: Some(info),
7606        });
7607        test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7608        test_helper.send_connected_event(random_bss_description!(Wpa2));
7609        test_helper.drain_cobalt_events(&mut test_fut);
7610
7611        let breakdowns_by_user_wait_time = test_helper
7612            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7613        assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7614        assert_eq!(
7615            breakdowns_by_user_wait_time[0].event_codes,
7616            vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan3Seconds as u32]
7617        );
7618    }
7619
7620    #[fuchsia::test]
7621    fn test_log_downtime_cobalt_metrics() {
7622        let (mut test_helper, mut test_fut) = setup_test();
7623        test_helper.send_connected_event(random_bss_description!(Wpa2));
7624        test_helper.drain_cobalt_events(&mut test_fut);
7625
7626        let info = DisconnectInfo {
7627            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
7628                reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
7629                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
7630            }),
7631            ..fake_disconnect_info()
7632        };
7633        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7634            track_subsequent_downtime: true,
7635            info: Some(info),
7636        });
7637        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7638
7639        test_helper.advance_by(zx::MonotonicDuration::from_minutes(42), test_fut.as_mut());
7640        // Indicate that there's no saved neighbor in vicinity
7641        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
7642            network_selection_type: NetworkSelectionType::Undirected,
7643            num_candidates: Ok(0),
7644            selected_count: 0,
7645        });
7646        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7647
7648        test_helper.advance_by(zx::MonotonicDuration::from_minutes(5), test_fut.as_mut());
7649        // Indicate that there's some saved neighbor in vicinity
7650        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
7651            network_selection_type: NetworkSelectionType::Undirected,
7652            num_candidates: Ok(5),
7653            selected_count: 1,
7654        });
7655        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7656
7657        test_helper.advance_by(zx::MonotonicDuration::from_minutes(7), test_fut.as_mut());
7658        // Reconnect
7659        test_helper.send_connected_event(random_bss_description!(Wpa2));
7660        test_helper.drain_cobalt_events(&mut test_fut);
7661
7662        let breakdowns_by_reason = test_helper
7663            .get_logged_metrics(metrics::DOWNTIME_BREAKDOWN_BY_DISCONNECT_REASON_METRIC_ID);
7664        assert_eq!(breakdowns_by_reason.len(), 1);
7665        assert_eq!(
7666            breakdowns_by_reason[0].event_codes,
7667            vec![3u32, metrics::ConnectivityWlanMetricDimensionDisconnectSource::Mlme as u32,]
7668        );
7669        assert_eq!(
7670            breakdowns_by_reason[0].payload,
7671            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(49).into_micros())
7672        );
7673    }
7674
7675    #[fuchsia::test]
7676    fn test_log_reconnect_cobalt_metrics() {
7677        let (mut test_helper, mut test_fut) = setup_test();
7678        test_helper.send_connected_event(random_bss_description!(Wpa2));
7679        test_helper.drain_cobalt_events(&mut test_fut);
7680
7681        // Send disconnect with non-roam cause.
7682        let info = DisconnectInfo {
7683            disconnect_source: fidl_sme::DisconnectSource::User(
7684                fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch,
7685            ),
7686            ..fake_disconnect_info()
7687        };
7688        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7689            track_subsequent_downtime: true,
7690            info: Some(info),
7691        });
7692        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7693
7694        test_helper.advance_by(zx::MonotonicDuration::from_seconds(3), test_fut.as_mut());
7695        // Reconnect.
7696        test_helper.send_connected_event(random_bss_description!(Wpa2));
7697        test_helper.drain_cobalt_events(&mut test_fut);
7698
7699        // Verify the reconnect duration was logged for a non-roam disconnect only.
7700        let metrics =
7701            test_helper.get_logged_metrics(metrics::NON_ROAM_RECONNECT_DURATION_METRIC_ID);
7702        assert_eq!(metrics.len(), 1);
7703        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(3_000_000));
7704        assert!(
7705            test_helper
7706                .get_logged_metrics(metrics::POLICY_ROAM_RECONNECT_DURATION_METRIC_ID)
7707                .is_empty()
7708        );
7709
7710        // Send a disconnect with a roaming cause.
7711        test_helper.clear_cobalt_events();
7712        let info = DisconnectInfo {
7713            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
7714                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
7715                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
7716            }),
7717            ..fake_disconnect_info()
7718        };
7719        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7720            track_subsequent_downtime: true,
7721            info: Some(info),
7722        });
7723        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7724        test_helper.advance_by(zx::MonotonicDuration::from_seconds(1), test_fut.as_mut());
7725        // Reconnect.
7726        test_helper.send_connected_event(random_bss_description!(Wpa2));
7727        test_helper.drain_cobalt_events(&mut test_fut);
7728
7729        // Verify the reconnect duration was NOT logged for a non-roam reconnect, since the cause
7730        // was a roam cause.
7731        assert!(
7732            test_helper
7733                .get_logged_metrics(metrics::NON_ROAM_RECONNECT_DURATION_METRIC_ID)
7734                .is_empty()
7735        );
7736        // Verify the reconnect duration is also NOT logged for a roam reconnect, despite the roam
7737        // cause, as roam reconnect durations are logged in the roam result event where we can
7738        // distinguish successful roams from failures.
7739        assert!(
7740            test_helper
7741                .get_logged_metrics(metrics::POLICY_ROAM_RECONNECT_DURATION_METRIC_ID)
7742                .is_empty()
7743        );
7744    }
7745
7746    #[fuchsia::test]
7747    fn test_log_device_connected_cobalt_metrics() {
7748        let (mut test_helper, mut test_fut) = setup_test();
7749
7750        let wmm_info = vec![0x80]; // U-APSD enabled
7751        #[rustfmt::skip]
7752        let rm_enabled_capabilities = vec![
7753            0x03, // link measurement and neighbor report enabled
7754            0x00, 0x00, 0x00, 0x00,
7755        ];
7756        #[rustfmt::skip]
7757        let ext_capabilities = vec![
7758            0x04, 0x00,
7759            0x08, // BSS transition supported
7760            0x00, 0x00, 0x00, 0x00, 0x40
7761        ];
7762        let bss_description = random_bss_description!(Wpa2,
7763            channel: Channel::new(157, Bandwidth::Cbw40, FiveGhz),
7764            ies_overrides: IesOverrides::new()
7765                .remove(IeType::WMM_PARAM)
7766                .set(IeType::WMM_INFO, wmm_info)
7767                .set(IeType::RM_ENABLED_CAPABILITIES, rm_enabled_capabilities)
7768                .set(IeType::MOBILITY_DOMAIN, vec![0x00; 3])
7769                .set(IeType::EXT_CAPABILITIES, ext_capabilities),
7770            bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
7771        );
7772        test_helper.send_connected_event(bss_description);
7773        test_helper.drain_cobalt_events(&mut test_fut);
7774
7775        let num_devices_connected =
7776            test_helper.get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID);
7777        assert_eq!(num_devices_connected.len(), 1);
7778        assert_eq!(num_devices_connected[0].payload, MetricEventPayload::Count(1));
7779
7780        let connected_security_type =
7781            test_helper.get_logged_metrics(metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID);
7782        assert_eq!(connected_security_type.len(), 1);
7783        assert_eq!(
7784            connected_security_type[0].event_codes,
7785            vec![
7786                metrics::ConnectedNetworkSecurityTypeMetricDimensionSecurityType::Wpa2Personal
7787                    as u32
7788            ]
7789        );
7790        assert_eq!(connected_security_type[0].payload, MetricEventPayload::Count(1));
7791
7792        let connected_apsd = test_helper
7793            .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID);
7794        assert_eq!(connected_apsd.len(), 1);
7795        assert_eq!(connected_apsd[0].payload, MetricEventPayload::Count(1));
7796
7797        let connected_link_measurement = test_helper.get_logged_metrics(
7798            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
7799        );
7800        assert_eq!(connected_link_measurement.len(), 1);
7801        assert_eq!(connected_link_measurement[0].payload, MetricEventPayload::Count(1));
7802
7803        let connected_neighbor_report = test_helper.get_logged_metrics(
7804            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
7805        );
7806        assert_eq!(connected_neighbor_report.len(), 1);
7807        assert_eq!(connected_neighbor_report[0].payload, MetricEventPayload::Count(1));
7808
7809        let connected_ft = test_helper
7810            .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID);
7811        assert_eq!(connected_ft.len(), 1);
7812        assert_eq!(connected_ft[0].payload, MetricEventPayload::Count(1));
7813
7814        let connected_bss_transition_mgmt = test_helper.get_logged_metrics(
7815            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
7816        );
7817        assert_eq!(connected_bss_transition_mgmt.len(), 1);
7818        assert_eq!(connected_bss_transition_mgmt[0].payload, MetricEventPayload::Count(1));
7819
7820        let breakdown_by_is_multi_bss = test_helper.get_logged_metrics(
7821            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
7822        );
7823        assert_eq!(breakdown_by_is_multi_bss.len(), 1);
7824        assert_eq!(
7825            breakdown_by_is_multi_bss[0].event_codes,
7826            vec![
7827                metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes
7828                    as u32
7829            ]
7830        );
7831        assert_eq!(breakdown_by_is_multi_bss[0].payload, MetricEventPayload::Count(1));
7832
7833        let breakdown_by_primary_channel = test_helper.get_logged_metrics(
7834            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
7835        );
7836        assert_eq!(breakdown_by_primary_channel.len(), 1);
7837        assert_eq!(breakdown_by_primary_channel[0].event_codes, vec![157]);
7838        assert_eq!(breakdown_by_primary_channel[0].payload, MetricEventPayload::Count(1));
7839
7840        let breakdown_by_channel_band = test_helper.get_logged_metrics(
7841            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
7842        );
7843        assert_eq!(breakdown_by_channel_band.len(), 1);
7844        assert_eq!(
7845            breakdown_by_channel_band[0].event_codes,
7846            vec![
7847                metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band5Ghz
7848                    as u32
7849            ]
7850        );
7851        assert_eq!(breakdown_by_channel_band[0].payload, MetricEventPayload::Count(1));
7852
7853        let ap_oui_connected =
7854            test_helper.get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID);
7855        assert_eq!(ap_oui_connected.len(), 1);
7856        assert_eq!(
7857            ap_oui_connected[0].payload,
7858            MetricEventPayload::StringValue("00F620".to_string())
7859        );
7860
7861        let network_is_likely_hidden =
7862            test_helper.get_logged_metrics(metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID);
7863        assert_eq!(network_is_likely_hidden.len(), 1);
7864        assert_eq!(network_is_likely_hidden[0].payload, MetricEventPayload::Count(1));
7865    }
7866
7867    #[fuchsia::test]
7868    fn test_log_device_connected_cobalt_metrics_ap_features_not_supported() {
7869        let (mut test_helper, mut test_fut) = setup_test();
7870
7871        let bss_description = random_bss_description!(Wpa2,
7872            ies_overrides: IesOverrides::new()
7873                .remove(IeType::WMM_PARAM)
7874                .remove(IeType::WMM_INFO)
7875                .remove(IeType::RM_ENABLED_CAPABILITIES)
7876                .remove(IeType::MOBILITY_DOMAIN)
7877                .remove(IeType::EXT_CAPABILITIES)
7878        );
7879        test_helper.send_connected_event(bss_description);
7880        test_helper.drain_cobalt_events(&mut test_fut);
7881
7882        let connected_apsd = test_helper
7883            .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID);
7884        assert_eq!(connected_apsd.len(), 0);
7885
7886        let connected_link_measurement = test_helper.get_logged_metrics(
7887            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
7888        );
7889        assert_eq!(connected_link_measurement.len(), 0);
7890
7891        let connected_neighbor_report = test_helper.get_logged_metrics(
7892            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
7893        );
7894        assert_eq!(connected_neighbor_report.len(), 0);
7895
7896        let connected_ft = test_helper
7897            .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID);
7898        assert_eq!(connected_ft.len(), 0);
7899
7900        let connected_bss_transition_mgmt = test_helper.get_logged_metrics(
7901            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
7902        );
7903        assert_eq!(connected_bss_transition_mgmt.len(), 0);
7904    }
7905
7906    #[test_case(metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID, None; "connect_to_likely_hidden_network")]
7907    #[test_case(metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID, None; "breakdown_by_is_multi_bss")]
7908    #[fuchsia::test(add_test_attr = false)]
7909    fn test_log_device_connected_cobalt_metrics_on_disconnect_and_periodically(
7910        metric_id: u32,
7911        payload: Option<Vec<MetricEvent>>,
7912    ) {
7913        let (mut test_helper, mut test_fut) = setup_test();
7914
7915        let bss_description = random_bss_description!(Wpa2,
7916            bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
7917        );
7918        test_helper.send_connected_event(bss_description);
7919        test_helper.drain_cobalt_events(&mut test_fut);
7920        test_helper.cobalt_events.clear();
7921
7922        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
7923
7924        // Verify that after 24 hours has passed, metric is logged at least once because
7925        // device is still connected
7926        let metrics = test_helper.get_logged_metrics(metric_id);
7927        assert!(!metrics.is_empty());
7928
7929        if let Some(payload) = payload {
7930            assert_eq_cobalt_events(metrics, payload)
7931        }
7932
7933        test_helper.cobalt_events.clear();
7934
7935        let info = fake_disconnect_info();
7936        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7937            track_subsequent_downtime: false,
7938            info: Some(info),
7939        });
7940        test_helper.drain_cobalt_events(&mut test_fut);
7941
7942        // Verify that on disconnect, device connected metric is also logged.
7943        let metrics = test_helper.get_logged_metrics(metric_id);
7944        assert_eq!(metrics.len(), 1);
7945    }
7946
7947    #[fuchsia::test]
7948    fn test_log_device_connected_cobalt_metrics_on_channel_switched() {
7949        let (mut test_helper, mut test_fut) = setup_test();
7950        let bss_description = random_bss_description!(Wpa2,
7951            channel: Channel::new(4, Bandwidth::Cbw20, TwoGhz),
7952        );
7953        test_helper.send_connected_event(bss_description);
7954        test_helper.drain_cobalt_events(&mut test_fut);
7955
7956        let breakdown_by_primary_channel = test_helper.get_logged_metrics(
7957            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
7958        );
7959        assert_eq!(breakdown_by_primary_channel.len(), 1);
7960        assert_eq!(breakdown_by_primary_channel[0].event_codes, vec![4]);
7961        assert_eq!(breakdown_by_primary_channel[0].payload, MetricEventPayload::Count(1));
7962
7963        let breakdown_by_channel_band = test_helper.get_logged_metrics(
7964            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
7965        );
7966        assert_eq!(breakdown_by_channel_band.len(), 1);
7967        assert_eq!(
7968            breakdown_by_channel_band[0].event_codes,
7969            vec![
7970                metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz
7971                    as u32
7972            ]
7973        );
7974        assert_eq!(breakdown_by_channel_band[0].payload, MetricEventPayload::Count(1));
7975
7976        // Clear out existing Cobalt metrics
7977        test_helper.cobalt_events.clear();
7978
7979        test_helper.telemetry_sender.send(TelemetryEvent::OnChannelSwitched {
7980            info: fidl_internal::ChannelSwitchInfo {
7981                new_primary_channel: fidl_ieee80211::ChannelNumber {
7982                    band: fidl_ieee80211::WlanBand::FiveGhz,
7983                    number: 157,
7984                },
7985                bandwidth: fidl_ieee80211::ChannelBandwidth::Cbw20,
7986                vht_secondary_80_channel: fidl_ieee80211::ChannelNumber {
7987                    band: fidl_ieee80211::WlanBand::FiveGhz,
7988                    number: 0,
7989                },
7990            },
7991        });
7992        test_helper.drain_cobalt_events(&mut test_fut);
7993
7994        // On channel switched, device connected metrics for the new channel and channel band
7995        // are logged.
7996        let breakdown_by_primary_channel = test_helper.get_logged_metrics(
7997            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
7998        );
7999        assert_eq!(breakdown_by_primary_channel.len(), 1);
8000        assert_eq!(breakdown_by_primary_channel[0].event_codes, vec![157]);
8001        assert_eq!(breakdown_by_primary_channel[0].payload, MetricEventPayload::Count(1));
8002
8003        let breakdown_by_channel_band = test_helper.get_logged_metrics(
8004            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
8005        );
8006        assert_eq!(breakdown_by_channel_band.len(), 1);
8007        assert_eq!(
8008            breakdown_by_channel_band[0].event_codes,
8009            vec![
8010                metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band5Ghz
8011                    as u32
8012            ]
8013        );
8014        assert_eq!(breakdown_by_channel_band[0].payload, MetricEventPayload::Count(1));
8015    }
8016
8017    #[fuchsia::test]
8018    fn test_active_scan_requested_metric() {
8019        let (mut test_helper, mut test_fut) = setup_test();
8020
8021        test_helper
8022            .telemetry_sender
8023            .send(TelemetryEvent::ActiveScanRequested { num_ssids_requested: 4 });
8024
8025        test_helper.drain_cobalt_events(&mut test_fut);
8026        let metrics = test_helper.get_logged_metrics(
8027            metrics::ACTIVE_SCAN_REQUESTED_FOR_NETWORK_SELECTION_MIGRATED_METRIC_ID,
8028        );
8029        assert_eq!(metrics.len(), 1);
8030        assert_eq!(metrics[0].event_codes, vec![metrics::ActiveScanRequestedForNetworkSelectionMigratedMetricDimensionActiveScanSsidsRequested::TwoToFour as u32]);
8031        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8032    }
8033
8034    #[fuchsia::test]
8035    fn test_log_device_performed_roaming_scan() {
8036        let (mut test_helper, mut test_fut) = setup_test();
8037
8038        // Send a roaming scan event
8039        test_helper.telemetry_sender.send(TelemetryEvent::PolicyRoamScan {
8040            reasons: vec![RoamReason::RssiBelowThreshold, RoamReason::SnrBelowThreshold],
8041        });
8042        test_helper.drain_cobalt_events(&mut test_fut);
8043
8044        // Check that the event was logged to cobalt.
8045        let metrics = test_helper.get_logged_metrics(metrics::POLICY_ROAM_SCAN_COUNT_METRIC_ID);
8046        assert_eq!(metrics.len(), 1);
8047        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8048
8049        // Check that an event was logged for each roam reason.
8050        let metrics = test_helper
8051            .get_logged_metrics(metrics::POLICY_ROAM_SCAN_COUNT_BY_ROAM_REASON_METRIC_ID);
8052        assert_eq!(metrics.len(), 2);
8053        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8054        assert_eq!(
8055            metrics[0].event_codes,
8056            vec![convert::convert_roam_reason_dimension(RoamReason::RssiBelowThreshold) as u32]
8057        );
8058        assert_eq!(metrics[1].payload, MetricEventPayload::Count(1));
8059        assert_eq!(
8060            metrics[1].event_codes,
8061            vec![convert::convert_roam_reason_dimension(RoamReason::SnrBelowThreshold) as u32]
8062        );
8063    }
8064
8065    #[fuchsia::test]
8066    fn test_log_policy_roam_attempt() {
8067        let (mut test_helper, mut test_fut) = setup_test();
8068
8069        // Send a roaming scan event
8070        let candidate = generate_random_scanned_candidate();
8071        test_helper.telemetry_sender.send(TelemetryEvent::PolicyRoamAttempt {
8072            request: PolicyRoamRequest {
8073                candidate,
8074                reasons: vec![RoamReason::RssiBelowThreshold, RoamReason::SnrBelowThreshold],
8075            },
8076            connected_duration: zx::Duration::from_hours(1),
8077        });
8078        test_helper.drain_cobalt_events(&mut test_fut);
8079
8080        let metrics = test_helper.get_logged_metrics(metrics::POLICY_ROAM_ATTEMPT_COUNT_METRIC_ID);
8081        assert_eq!(metrics.len(), 1);
8082        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8083
8084        // Check that an event was logged for each roam reason.
8085        let metrics = test_helper
8086            .get_logged_metrics(metrics::POLICY_ROAM_ATTEMPT_COUNT_BY_ROAM_REASON_METRIC_ID);
8087        assert_eq!(metrics.len(), 2);
8088        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8089        assert_eq!(
8090            metrics[0].event_codes,
8091            vec![convert::convert_roam_reason_dimension(RoamReason::RssiBelowThreshold) as u32]
8092        );
8093        assert_eq!(metrics[1].payload, MetricEventPayload::Count(1));
8094        assert_eq!(
8095            metrics[1].event_codes,
8096            vec![convert::convert_roam_reason_dimension(RoamReason::SnrBelowThreshold) as u32]
8097        );
8098
8099        // Check that a metric was logged for the connedted duration before roaming
8100        let metrics = test_helper.get_logged_metrics(
8101            metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
8102        );
8103        assert_eq!(metrics.len(), 2);
8104        assert_eq!(metrics.len(), 2);
8105        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(60));
8106        assert_eq!(
8107            metrics[0].event_codes,
8108            vec![convert::convert_roam_reason_dimension(RoamReason::RssiBelowThreshold) as u32]
8109        );
8110        assert_eq!(metrics[1].payload, MetricEventPayload::IntegerValue(60));
8111        assert_eq!(
8112            metrics[1].event_codes,
8113            vec![convert::convert_roam_reason_dimension(RoamReason::SnrBelowThreshold) as u32]
8114        );
8115    }
8116
8117    /// Helper function for policy roam success rate tests
8118    fn log_policy_roam_attempt_and_result(
8119        test_helper: &mut TestHelper,
8120        is_success: bool,
8121        reasons: Vec<RoamReason>,
8122    ) {
8123        let status_code = if is_success {
8124            fidl_ieee80211::StatusCode::Success
8125        } else {
8126            fidl_ieee80211::StatusCode::RefusedReasonUnspecified
8127        };
8128
8129        // Log roam attempt
8130        let request = PolicyRoamRequest { candidate: generate_random_scanned_candidate(), reasons };
8131        let event = TelemetryEvent::PolicyRoamAttempt {
8132            request: request.clone(),
8133            connected_duration: zx::MonotonicDuration::from_hours(1),
8134        };
8135        test_helper.telemetry_sender.send(event);
8136
8137        // Log roam result with status code
8138        let result = fidl_sme::RoamResult {
8139            bssid: [1, 1, 1, 1, 1, 1],
8140            status_code,
8141            original_association_maintained: false,
8142            bss_description: Some(Box::new(random_fidl_bss_description!())),
8143            disconnect_info: None,
8144            is_credential_rejected: false,
8145        };
8146
8147        let event = TelemetryEvent::PolicyInitiatedRoamResult {
8148            iface_id: IFACE_ID,
8149            result,
8150            updated_ap_state: random_bss_description!().into(),
8151            original_ap_state: Box::new(random_bss_description!().into()),
8152            request: Box::new(request.clone()),
8153            request_time: fasync::MonotonicInstant::now(),
8154            result_time: fasync::MonotonicInstant::now(),
8155        };
8156        test_helper.telemetry_sender.send(event);
8157    }
8158
8159    #[fuchsia::test]
8160    fn test_log_policy_roam_success_rate_cobalt_metrics() {
8161        let (mut test_helper, mut test_fut) = setup_test();
8162        test_helper.send_connected_event(random_bss_description!(Wpa1));
8163
8164        // Log two roam successes
8165        log_policy_roam_attempt_and_result(
8166            &mut test_helper,
8167            true,
8168            vec![RoamReason::RssiBelowThreshold],
8169        );
8170        log_policy_roam_attempt_and_result(
8171            &mut test_helper,
8172            true,
8173            vec![RoamReason::RssiBelowThreshold],
8174        );
8175
8176        // Log one roam failure
8177        log_policy_roam_attempt_and_result(
8178            &mut test_helper,
8179            false,
8180            vec![RoamReason::RssiBelowThreshold],
8181        );
8182
8183        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
8184
8185        let metrics = test_helper.get_logged_metrics(metrics::POLICY_ROAM_SUCCESS_RATE_METRIC_ID);
8186        assert_eq!(metrics.len(), 1);
8187        assert_eq_cobalt_events(
8188            metrics,
8189            vec![MetricEvent {
8190                metric_id: metrics::POLICY_ROAM_SUCCESS_RATE_METRIC_ID,
8191                event_codes: vec![],
8192                payload: MetricEventPayload::IntegerValue(6666), // 66.66% success rate
8193            }],
8194        );
8195    }
8196
8197    #[fuchsia::test]
8198    fn test_log_policy_roam_success_rate_by_roam_reason_cobalt_metrics() {
8199        let (mut test_helper, mut test_fut) = setup_test();
8200        test_helper.send_connected_event(random_bss_description!(Wpa1));
8201
8202        // Log two roam successes with different reason event code vectors
8203        log_policy_roam_attempt_and_result(
8204            &mut test_helper,
8205            true,
8206            vec![RoamReason::RssiBelowThreshold],
8207        );
8208        log_policy_roam_attempt_and_result(
8209            &mut test_helper,
8210            true,
8211            vec![RoamReason::RssiBelowThreshold, RoamReason::SnrBelowThreshold],
8212        );
8213
8214        // Log one roam failure
8215        log_policy_roam_attempt_and_result(
8216            &mut test_helper,
8217            false,
8218            vec![RoamReason::RssiBelowThreshold],
8219        );
8220
8221        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
8222
8223        let metrics = test_helper
8224            .get_logged_metrics(metrics::POLICY_ROAM_SUCCESS_RATE_BY_ROAM_REASON_METRIC_ID);
8225        assert_eq!(metrics.len(), 2);
8226        assert_eq_cobalt_events(
8227            metrics,
8228            vec![
8229                MetricEvent {
8230                    metric_id: metrics::POLICY_ROAM_SUCCESS_RATE_BY_ROAM_REASON_METRIC_ID,
8231                    event_codes: vec![convert::convert_roam_reason_dimension(
8232                        RoamReason::RssiBelowThreshold,
8233                    ) as u32],
8234                    payload: MetricEventPayload::IntegerValue(6666), // 66.66% success for RssiBelowThreshold
8235                },
8236                MetricEvent {
8237                    metric_id: metrics::POLICY_ROAM_SUCCESS_RATE_BY_ROAM_REASON_METRIC_ID,
8238                    event_codes: vec![convert::convert_roam_reason_dimension(
8239                        RoamReason::SnrBelowThreshold,
8240                    ) as u32],
8241                    payload: MetricEventPayload::IntegerValue(10000), // 100% success for SnrBelowThreshold
8242                },
8243            ],
8244        );
8245    }
8246
8247    #[fuchsia::test]
8248    fn test_connection_enabled_duration_metric() {
8249        let (mut test_helper, mut test_fut) = setup_test();
8250
8251        test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8252        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8253        test_helper.advance_by(zx::MonotonicDuration::from_seconds(10), test_fut.as_mut());
8254        test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8255
8256        test_helper.drain_cobalt_events(&mut test_fut);
8257        let metrics = test_helper
8258            .get_logged_metrics(metrics::CLIENT_CONNECTIONS_ENABLED_DURATION_MIGRATED_METRIC_ID);
8259        assert_eq!(metrics.len(), 1);
8260        assert_eq!(
8261            metrics[0].payload,
8262            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_seconds(10).into_micros())
8263        );
8264    }
8265
8266    #[fuchsia::test]
8267    fn test_stop_ap_metric() {
8268        let (mut test_helper, mut test_fut) = setup_test();
8269
8270        test_helper.telemetry_sender.send(TelemetryEvent::StopAp {
8271            enabled_duration: zx::MonotonicDuration::from_seconds(50),
8272        });
8273
8274        test_helper.drain_cobalt_events(&mut test_fut);
8275        let metrics = test_helper
8276            .get_logged_metrics(metrics::ACCESS_POINT_ENABLED_DURATION_MIGRATED_METRIC_ID);
8277        assert_eq!(metrics.len(), 1);
8278        assert_eq!(
8279            metrics[0].payload,
8280            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_seconds(50).into_micros())
8281        );
8282    }
8283
8284    #[derive(PartialEq)]
8285    enum CreateMetricsLoggerFailureMode {
8286        None,
8287        FactoryRequest,
8288        ApiFailure,
8289    }
8290
8291    #[test_case(CreateMetricsLoggerFailureMode::None)]
8292    #[test_case(CreateMetricsLoggerFailureMode::FactoryRequest)]
8293    #[test_case(CreateMetricsLoggerFailureMode::ApiFailure)]
8294    #[fuchsia::test]
8295    fn test_create_metrics_logger(failure_mode: CreateMetricsLoggerFailureMode) {
8296        let mut exec = fasync::TestExecutor::new();
8297        let (factory_proxy, mut factory_stream) = fidl::endpoints::create_proxy_and_stream::<
8298            fidl_fuchsia_metrics::MetricEventLoggerFactoryMarker,
8299        >();
8300
8301        let fut = create_metrics_logger(&factory_proxy);
8302        let mut fut = pin!(fut);
8303
8304        // First, test the case where the factory service cannot be reached and expect an error.
8305        if failure_mode == CreateMetricsLoggerFailureMode::FactoryRequest {
8306            drop(factory_stream);
8307            assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(Err(_)));
8308            return;
8309        }
8310
8311        // If the test case is intended to allow the factory service to be contacted, run the
8312        // request future until stalled.
8313        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
8314
8315        let request = exec.run_until_stalled(&mut factory_stream.next());
8316        assert_matches!(
8317            request,
8318            Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerFactoryRequest::CreateMetricEventLogger {
8319                project_spec: fidl_fuchsia_metrics::ProjectSpec {
8320                    customer_id: None,
8321                    project_id: Some(metrics::PROJECT_ID),
8322                    ..
8323                },
8324                responder,
8325                ..
8326            }))) => {
8327                match failure_mode {
8328                    CreateMetricsLoggerFailureMode::FactoryRequest => panic!("The factory request failure should have been handled already."),
8329                    CreateMetricsLoggerFailureMode::None => responder.send(Ok(())).expect("failed to send response"),
8330                    CreateMetricsLoggerFailureMode::ApiFailure => responder.send(Err(fidl_fuchsia_metrics::Error::InvalidArguments)).expect("failed to send response"),
8331                }
8332            }
8333        );
8334
8335        // The future should run to completion and the output will vary depending on the specified
8336        // failure mode.
8337        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(result) => {
8338            match failure_mode {
8339                CreateMetricsLoggerFailureMode::FactoryRequest => panic!("The factory request failure should have been handled already."),
8340                CreateMetricsLoggerFailureMode::None => assert_matches!(result, Ok(_)),
8341                CreateMetricsLoggerFailureMode::ApiFailure => assert_matches!(result, Err(_))
8342            }
8343        });
8344    }
8345
8346    #[test_case(ScanIssue::ScanFailure, metrics::CLIENT_SCAN_FAILURE_METRIC_ID)]
8347    #[test_case(ScanIssue::AbortedScan, metrics::ABORTED_SCAN_METRIC_ID)]
8348    #[test_case(ScanIssue::EmptyScanResults, metrics::EMPTY_SCAN_RESULTS_METRIC_ID)]
8349    #[fuchsia::test(add_test_attr = false)]
8350    fn test_scan_defect_metrics(scan_issue: ScanIssue, expected_metric_id: u32) {
8351        let (mut test_helper, mut test_fut) = setup_test();
8352
8353        let event = TelemetryEvent::ScanEvent {
8354            inspect_data: ScanEventInspectData::new(),
8355            scan_defects: vec![scan_issue],
8356        };
8357
8358        // Send a notification that interface creation has failed.
8359        test_helper.telemetry_sender.send(event);
8360
8361        // Run the telemetry loop until it stalls.
8362        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8363
8364        // Expect that Cobalt has been notified of the metric
8365        test_helper.drain_cobalt_events(&mut test_fut);
8366        let logged_metrics = test_helper.get_logged_metrics(expected_metric_id);
8367        assert_eq!(logged_metrics.len(), 1);
8368    }
8369
8370    #[fuchsia::test]
8371    fn test_log_ap_start_failure() {
8372        let (mut test_helper, mut test_fut) = setup_test();
8373
8374        // Send a notification that starting the AP has failed.
8375        test_helper.telemetry_sender.send(TelemetryEvent::StartApResult(Err(())));
8376
8377        // Run the telemetry loop until it stalls.
8378        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8379
8380        // Expect that Cobalt has been notified of the AP start failure.
8381        test_helper.drain_cobalt_events(&mut test_fut);
8382        let logged_metrics = test_helper.get_logged_metrics(metrics::AP_START_FAILURE_METRIC_ID);
8383        assert_eq!(logged_metrics.len(), 1);
8384    }
8385
8386    #[test_case(
8387        RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset),
8388        metrics::RecoveryOccurrenceMetricDimensionReason::InterfaceCreationFailure ;
8389        "log recovery event for iface creation failure"
8390    )]
8391    #[test_case(
8392        RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset),
8393        metrics::RecoveryOccurrenceMetricDimensionReason::InterfaceDestructionFailure ;
8394        "log recovery event for iface destruction failure"
8395    )]
8396    #[test_case(
8397        RecoveryReason::ConnectFailure(ClientRecoveryMechanism::Disconnect),
8398        metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure ;
8399        "log recovery event for connect failure"
8400    )]
8401    #[test_case(
8402        RecoveryReason::StartApFailure(ApRecoveryMechanism::StopAp),
8403        metrics::RecoveryOccurrenceMetricDimensionReason::ApStartFailure ;
8404        "log recovery event for start AP failure"
8405    )]
8406    #[test_case(
8407        RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect),
8408        metrics::RecoveryOccurrenceMetricDimensionReason::ScanFailure ;
8409        "log recovery event for scan failure"
8410    )]
8411    #[test_case(
8412        RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect),
8413         metrics::RecoveryOccurrenceMetricDimensionReason::ScanCancellation ;
8414        "log recovery event for scan cancellation"
8415    )]
8416    #[test_case(
8417        RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect),
8418        metrics::RecoveryOccurrenceMetricDimensionReason::ScanResultsEmpty ;
8419        "log recovery event for empty scan results"
8420    )]
8421    #[fuchsia::test(add_test_attr = false)]
8422    fn test_log_recovery_occurrence(
8423        reason: RecoveryReason,
8424        expected_dimension: metrics::RecoveryOccurrenceMetricDimensionReason,
8425    ) {
8426        let (mut test_helper, mut test_fut) = setup_test();
8427
8428        // Send the recovery event metric.
8429        test_helper.telemetry_sender.send(TelemetryEvent::RecoveryEvent { reason });
8430
8431        // Run the telemetry loop until it stalls.
8432        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8433
8434        // Expect that Cobalt has been notified of the recovery event
8435        assert_matches!(
8436            test_helper.exec.run_until_stalled(&mut test_helper.cobalt_stream.next()),
8437            Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerRequest::LogOccurrence {
8438                metric_id, event_codes, responder, ..
8439            }))) => {
8440                assert_eq!(metric_id, metrics::RECOVERY_OCCURRENCE_METRIC_ID);
8441                assert_eq!(event_codes, vec![expected_dimension.as_event_code()]);
8442
8443                assert!(responder.send(Ok(())).is_ok());
8444        });
8445    }
8446
8447    #[test_case(
8448        RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset),
8449        RecoveryOutcome::Success,
8450        metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
8451        vec![RecoveryOutcome::Success as u32] ;
8452        "create iface fixed by resetting PHY"
8453    )]
8454    #[test_case(
8455        RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset),
8456        RecoveryOutcome::Failure,
8457        metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
8458        vec![RecoveryOutcome::Failure as u32] ;
8459        "create iface not fixed by resetting PHY"
8460    )]
8461    #[test_case(
8462        RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset),
8463        RecoveryOutcome::Success,
8464        metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
8465        vec![RecoveryOutcome::Success as u32] ;
8466        "destroy iface fixed by resetting PHY"
8467    )]
8468    #[test_case(
8469        RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset),
8470        RecoveryOutcome::Failure,
8471        metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
8472        vec![RecoveryOutcome::Failure as u32] ;
8473        "destroy iface not fixed by resetting PHY"
8474    )]
8475    #[test_case(
8476        RecoveryReason::ConnectFailure(ClientRecoveryMechanism::Disconnect),
8477        RecoveryOutcome::Success,
8478        metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8479        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8480        "connect works after disconnecting"
8481    )]
8482    #[test_case(
8483        RecoveryReason::ConnectFailure(ClientRecoveryMechanism::DestroyIface),
8484        RecoveryOutcome::Success,
8485        metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8486        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8487        "connect works after destroying iface"
8488    )]
8489    #[test_case(
8490        RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset),
8491        RecoveryOutcome::Success,
8492        metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8493        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8494        "connect works after resetting PHY"
8495    )]
8496    #[test_case(
8497        RecoveryReason::ConnectFailure(ClientRecoveryMechanism::Disconnect),
8498        RecoveryOutcome::Failure,
8499        metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8500        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8501        "connect still fails after disconnecting"
8502    )]
8503    #[test_case(
8504        RecoveryReason::ConnectFailure(ClientRecoveryMechanism::DestroyIface),
8505        RecoveryOutcome::Failure,
8506        metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8507        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8508        "connect still fails after destroying iface"
8509    )]
8510    #[test_case(
8511        RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset),
8512        RecoveryOutcome::Failure,
8513        metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8514        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8515        "connect still fails after resetting PHY"
8516    )]
8517    #[test_case(
8518        RecoveryReason::StartApFailure(ApRecoveryMechanism::StopAp),
8519        RecoveryOutcome::Success,
8520        metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
8521        vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::StopAp as u32] ;
8522        "start AP works after stopping AP"
8523    )]
8524    #[test_case(
8525        RecoveryReason::StartApFailure(ApRecoveryMechanism::DestroyIface),
8526        RecoveryOutcome::Success,
8527        metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
8528        vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::DestroyIface as u32] ;
8529        "start AP works after destroying iface"
8530    )]
8531    #[test_case(
8532        RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy),
8533        RecoveryOutcome::Success,
8534        metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
8535        vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::ResetPhy as u32] ;
8536        "start AP works after resetting PHY"
8537    )]
8538    #[test_case(
8539        RecoveryReason::StartApFailure(ApRecoveryMechanism::StopAp),
8540        RecoveryOutcome::Failure,
8541        metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
8542        vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::StopAp as u32] ;
8543        "start AP still fails after stopping AP"
8544    )]
8545    #[test_case(
8546        RecoveryReason::StartApFailure(ApRecoveryMechanism::DestroyIface),
8547        RecoveryOutcome::Failure,
8548        metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
8549        vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::DestroyIface as u32] ;
8550        "start AP still fails after destroying iface"
8551    )]
8552    #[test_case(
8553        RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy),
8554        RecoveryOutcome::Failure,
8555        metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
8556        vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::ResetPhy as u32] ;
8557        "start AP still fails after resetting PHY"
8558    )]
8559    #[test_case(
8560        RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect),
8561        RecoveryOutcome::Success,
8562        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8563        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8564        "scan works after disconnecting"
8565    )]
8566    #[test_case(
8567        RecoveryReason::ScanFailure(ClientRecoveryMechanism::DestroyIface),
8568        RecoveryOutcome::Success,
8569        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8570        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8571        "scan works after destroying iface"
8572    )]
8573    #[test_case(
8574        RecoveryReason::ScanFailure(ClientRecoveryMechanism::PhyReset),
8575        RecoveryOutcome::Success,
8576        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8577        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8578        "scan works after resetting PHY"
8579    )]
8580    #[test_case(
8581        RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect),
8582        RecoveryOutcome::Failure,
8583        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8584        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8585        "scan still fails after disconnecting"
8586    )]
8587    #[test_case(
8588        RecoveryReason::ScanFailure(ClientRecoveryMechanism::DestroyIface),
8589        RecoveryOutcome::Failure,
8590        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8591        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8592        "scan still fails after destroying iface"
8593    )]
8594    #[test_case(
8595        RecoveryReason::ScanFailure(ClientRecoveryMechanism::PhyReset),
8596        RecoveryOutcome::Failure,
8597        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8598        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8599        "scan still fails after resetting PHY"
8600    )]
8601    #[test_case(
8602        RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect),
8603        RecoveryOutcome::Success,
8604        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
8605        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8606        "scan is no longer cancelled after disconnecting"
8607    )]
8608    #[test_case(
8609        RecoveryReason::ScanCancellation(ClientRecoveryMechanism::DestroyIface),
8610        RecoveryOutcome::Success,
8611        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
8612        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8613        "scan is no longer cancelled after destroying iface"
8614    )]
8615    #[test_case(
8616        RecoveryReason::ScanCancellation(ClientRecoveryMechanism::PhyReset),
8617        RecoveryOutcome::Success,
8618        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
8619        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8620        "scan is no longer cancelled after resetting PHY"
8621    )]
8622    #[test_case(
8623        RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect),
8624        RecoveryOutcome::Failure,
8625        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
8626        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8627        "scan is still cancelled after disconnect"
8628    )]
8629    #[test_case(
8630        RecoveryReason::ScanCancellation(ClientRecoveryMechanism::DestroyIface),
8631        RecoveryOutcome::Failure,
8632        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
8633        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8634        "scan is still cancelled after destroying iface"
8635    )]
8636    #[test_case(
8637        RecoveryReason::ScanCancellation(ClientRecoveryMechanism::PhyReset),
8638        RecoveryOutcome::Failure,
8639        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
8640        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8641        "scan is still cancelled after resetting PHY"
8642    )]
8643    #[test_case(
8644        RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect),
8645        RecoveryOutcome::Success,
8646        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
8647        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8648        "scan results not empty after disconnect"
8649    )]
8650    #[test_case(
8651        RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::DestroyIface),
8652        RecoveryOutcome::Success,
8653        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
8654        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8655        "scan results not empty after destroy iface"
8656    )]
8657    #[test_case(
8658        RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::PhyReset),
8659        RecoveryOutcome::Success,
8660        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
8661        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8662        "scan results not empty after PHY reset"
8663    )]
8664    #[test_case(
8665        RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect),
8666        RecoveryOutcome::Failure,
8667        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
8668        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8669        "scan results still empty after disconnect"
8670    )]
8671    #[test_case(
8672        RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::DestroyIface),
8673        RecoveryOutcome::Failure,
8674        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
8675        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8676        "scan results still empty after destroy iface"
8677    )]
8678    #[test_case(
8679        RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::PhyReset),
8680        RecoveryOutcome::Failure,
8681        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
8682        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8683        "scan results still empty after PHY reset"
8684    )]
8685    #[fuchsia::test(add_test_attr = false)]
8686    fn test_log_post_recovery_result(
8687        reason: RecoveryReason,
8688        outcome: RecoveryOutcome,
8689        expected_metric_id: u32,
8690        expected_event_codes: Vec<u32>,
8691    ) {
8692        let mut exec = fasync::TestExecutor::new();
8693
8694        // Construct a StatsLogger
8695        let (cobalt_proxy, mut cobalt_stream) =
8696            create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
8697
8698        let inspector = Inspector::default();
8699        let inspect_node = inspector.root().create_child("stats");
8700
8701        let mut stats_logger = StatsLogger::new(cobalt_proxy, &inspect_node);
8702
8703        // Log the test telemetry event.
8704        let fut = stats_logger.log_post_recovery_result(reason, outcome);
8705        let mut fut = pin!(fut);
8706        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
8707
8708        // Verify the metric that was emitted.
8709        assert_matches!(
8710            exec.run_until_stalled(&mut cobalt_stream.next()),
8711            Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerRequest::LogOccurrence {
8712                metric_id, event_codes, responder, ..
8713            }))) => {
8714                assert_eq!(metric_id, expected_metric_id);
8715                assert_eq!(event_codes, expected_event_codes);
8716
8717                assert!(responder.send(Ok(())).is_ok());
8718        });
8719
8720        // The future should complete.
8721        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
8722    }
8723
8724    #[fuchsia::test]
8725    fn test_post_recovery_connect_success() {
8726        let (mut test_helper, mut test_fut) = setup_test();
8727
8728        // Send the recovery event metric.
8729        let reason = RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset);
8730        let event = TelemetryEvent::RecoveryEvent { reason };
8731        test_helper.telemetry_sender.send(event);
8732
8733        // Run the telemetry loop until it stalls.
8734        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8735
8736        // Expect that Cobalt has been notified of the recovery event
8737        test_helper.drain_cobalt_events(&mut test_fut);
8738        let logged_metrics = test_helper.get_logged_metrics(metrics::RECOVERY_OCCURRENCE_METRIC_ID);
8739        assert_eq!(logged_metrics.len(), 1);
8740
8741        // Verify the reason dimension.
8742        assert_eq!(
8743            logged_metrics[0].event_codes,
8744            vec![
8745                metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure
8746                    .as_event_code()
8747            ]
8748        );
8749
8750        // Send a successful connect result.
8751        test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
8752            iface_id: IFACE_ID,
8753            policy_connect_reason: Some(
8754                client::types::ConnectReason::RetryAfterFailedConnectAttempt,
8755            ),
8756            result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
8757            multiple_bss_candidates: true,
8758            ap_state: random_bss_description!(Wpa1).into(),
8759            network_is_likely_hidden: false,
8760        });
8761
8762        // Run the telemetry loop until it stalls.
8763        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8764
8765        // Verify the connect post-recovery success metric was logged.
8766        test_helper.drain_cobalt_events(&mut test_fut);
8767        let logged_metrics =
8768            test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
8769        assert_eq!(
8770            logged_metrics[0].event_codes,
8771            vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32]
8772        );
8773
8774        // Verify a subsequent connect result does not cause another metric to be logged.
8775        test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
8776            iface_id: IFACE_ID,
8777            policy_connect_reason: Some(
8778                client::types::ConnectReason::RetryAfterFailedConnectAttempt,
8779            ),
8780            result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
8781            multiple_bss_candidates: true,
8782            ap_state: random_bss_description!(Wpa1).into(),
8783            network_is_likely_hidden: false,
8784        });
8785
8786        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8787
8788        test_helper.cobalt_events = Vec::new();
8789        test_helper.drain_cobalt_events(&mut test_fut);
8790        let logged_metrics =
8791            test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
8792        assert!(logged_metrics.is_empty());
8793    }
8794
8795    #[fuchsia::test]
8796    fn test_post_recovery_connect_failure() {
8797        let (mut test_helper, mut test_fut) = setup_test();
8798
8799        // Send the recovery event metric.
8800        let reason = RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset);
8801        let event = TelemetryEvent::RecoveryEvent { reason };
8802        test_helper.telemetry_sender.send(event);
8803
8804        // Run the telemetry loop until it stalls.
8805        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8806
8807        // Expect that Cobalt has been notified of the recovery event
8808        test_helper.drain_cobalt_events(&mut test_fut);
8809        let logged_metrics = test_helper.get_logged_metrics(metrics::RECOVERY_OCCURRENCE_METRIC_ID);
8810        assert_eq!(logged_metrics.len(), 1);
8811
8812        // Verify the reason dimension.
8813        assert_eq!(
8814            logged_metrics[0].event_codes,
8815            vec![
8816                metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure
8817                    .as_event_code()
8818            ]
8819        );
8820
8821        // Send a failed connect result.
8822        test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
8823            iface_id: IFACE_ID,
8824            policy_connect_reason: Some(
8825                client::types::ConnectReason::RetryAfterFailedConnectAttempt,
8826            ),
8827            result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
8828            multiple_bss_candidates: true,
8829            ap_state: random_bss_description!(Wpa1).into(),
8830            network_is_likely_hidden: false,
8831        });
8832
8833        // Run the telemetry loop until it stalls.
8834        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8835
8836        // Verify the connect post-recovery failure metric was logged.
8837        test_helper.drain_cobalt_events(&mut test_fut);
8838        let logged_metrics =
8839            test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
8840        assert_eq!(
8841            logged_metrics[0].event_codes,
8842            vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32]
8843        );
8844
8845        // Verify a subsequent connect result does not cause another metric to be logged.
8846        test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
8847            iface_id: IFACE_ID,
8848            policy_connect_reason: Some(
8849                client::types::ConnectReason::RetryAfterFailedConnectAttempt,
8850            ),
8851            result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
8852            multiple_bss_candidates: true,
8853            ap_state: random_bss_description!(Wpa1).into(),
8854            network_is_likely_hidden: false,
8855        });
8856
8857        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8858
8859        test_helper.cobalt_events = Vec::new();
8860        test_helper.drain_cobalt_events(&mut test_fut);
8861        let logged_metrics =
8862            test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
8863        assert!(logged_metrics.is_empty());
8864    }
8865
8866    fn test_generic_post_recovery_event(
8867        recovery_event: TelemetryEvent,
8868        post_recovery_event: TelemetryEvent,
8869        duplicate_check_event: TelemetryEvent,
8870        expected_metric_id: u32,
8871        dimensions: Vec<u32>,
8872    ) {
8873        let (mut test_helper, mut test_fut) = setup_test();
8874        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(1));
8875
8876        // Send the recovery event metric
8877        test_helper.telemetry_sender.send(recovery_event);
8878        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8879
8880        // Send the post-recovery result metric
8881        test_helper.telemetry_sender.send(post_recovery_event);
8882        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8883
8884        // Get the metric that was logged and verify that it was constructed properly.
8885        test_helper.drain_cobalt_events(&mut test_fut);
8886        let logged_metrics = test_helper.get_logged_metrics(expected_metric_id);
8887
8888        assert_eq!(logged_metrics.len(), 1);
8889        assert_eq!(logged_metrics[0].event_codes, dimensions);
8890
8891        // Re-send the result metric and verify that nothing new was logged.
8892        test_helper.cobalt_events = Vec::new();
8893        test_helper.telemetry_sender.send(duplicate_check_event);
8894        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8895        let logged_metrics = test_helper.get_logged_metrics(expected_metric_id);
8896        assert!(logged_metrics.is_empty());
8897
8898        // If the recovery was successful, ensure that the last successful recovery time has been
8899        // updated.  If it was not successful, the last recovery time should not have been changed.
8900        if dimensions[0] == RecoveryOutcome::Success.as_event_code() {
8901            assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
8902                stats: contains {
8903                    last_successful_recovery: 1_u64,
8904                    successful_recoveries: 1_u64
8905                }
8906            });
8907        } else {
8908            assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
8909                stats: contains {
8910                    last_successful_recovery: 0_u64,
8911                    successful_recoveries: 0_u64
8912                }
8913            });
8914        }
8915    }
8916
8917    #[test_case(
8918        TelemetryEvent::RecoveryEvent {
8919            reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect)
8920        },
8921        TelemetryEvent::ScanEvent {
8922            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8923            scan_defects: vec![]
8924        },
8925        TelemetryEvent::ScanEvent {
8926            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8927            scan_defects: vec![]
8928        },
8929        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8930        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8931        "Scan succeeds after recovery with no other defects"
8932    )]
8933    #[test_case(
8934        TelemetryEvent::RecoveryEvent {
8935            reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect)
8936        },
8937        TelemetryEvent::ScanEvent {
8938            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8939            scan_defects: vec![ScanIssue::ScanFailure]
8940        },
8941        TelemetryEvent::ScanEvent {
8942            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8943            scan_defects: vec![ScanIssue::ScanFailure]
8944        },
8945        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8946        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8947        "Scan still fails following recovery"
8948    )]
8949    #[test_case(
8950        TelemetryEvent::RecoveryEvent {
8951            reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::DestroyIface)
8952        },
8953        TelemetryEvent::ScanEvent {
8954            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8955            scan_defects: vec![ScanIssue::AbortedScan]
8956        },
8957        TelemetryEvent::ScanEvent {
8958            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8959            scan_defects: vec![ScanIssue::AbortedScan]
8960        },
8961        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8962        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8963        "Scan succeeds after recovery but the scan was cancelled"
8964    )]
8965    #[test_case(
8966        TelemetryEvent::RecoveryEvent {
8967            reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::PhyReset)
8968        },
8969        TelemetryEvent::ScanEvent {
8970            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8971            scan_defects: vec![ScanIssue::EmptyScanResults]
8972        },
8973        TelemetryEvent::ScanEvent {
8974            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8975            scan_defects: vec![ScanIssue::EmptyScanResults]
8976        },
8977        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8978        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8979        "Scan succeeds after recovery but the results are empty"
8980    )]
8981    #[test_case(
8982        TelemetryEvent::RecoveryEvent {
8983            reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect)
8984        },
8985        TelemetryEvent::ScanEvent {
8986            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8987            scan_defects: vec![]
8988        },
8989        TelemetryEvent::ScanEvent {
8990            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8991            scan_defects: vec![]
8992        },
8993        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
8994        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8995        "Scan no longer cancelled after recovery"
8996    )]
8997    #[test_case(
8998        TelemetryEvent::RecoveryEvent {
8999            reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect)
9000        },
9001        TelemetryEvent::ScanEvent {
9002            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9003            scan_defects: vec![ScanIssue::ScanFailure]
9004        },
9005        TelemetryEvent::ScanEvent {
9006            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9007            scan_defects: vec![ScanIssue::ScanFailure]
9008        },
9009        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9010        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9011        "Scan not cancelled after recovery but fails instead"
9012    )]
9013    #[test_case(
9014        TelemetryEvent::RecoveryEvent {
9015            reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::DestroyIface)
9016        },
9017        TelemetryEvent::ScanEvent {
9018            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9019            scan_defects: vec![ScanIssue::AbortedScan]
9020        },
9021        TelemetryEvent::ScanEvent {
9022            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9023            scan_defects: vec![ScanIssue::AbortedScan]
9024        },
9025        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9026        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9027        "Scan still cancelled after recovery"
9028    )]
9029    #[test_case(
9030        TelemetryEvent::RecoveryEvent {
9031            reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::PhyReset)
9032        },
9033        TelemetryEvent::ScanEvent {
9034            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9035            scan_defects: vec![ScanIssue::EmptyScanResults]
9036        },
9037        TelemetryEvent::ScanEvent {
9038            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9039            scan_defects: vec![ScanIssue::EmptyScanResults]
9040        },
9041        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9042        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9043        "Scan not cancelled after recovery but results are empty"
9044    )]
9045    #[test_case(
9046        TelemetryEvent::RecoveryEvent {
9047            reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect)
9048        },
9049        TelemetryEvent::ScanEvent {
9050            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9051            scan_defects: vec![]
9052        },
9053        TelemetryEvent::ScanEvent {
9054            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9055            scan_defects: vec![]
9056        },
9057        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9058        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9059        "Scan results not empty after recovery and no other errors"
9060    )]
9061    #[test_case(
9062        TelemetryEvent::RecoveryEvent {
9063            reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect)
9064        },
9065        TelemetryEvent::ScanEvent {
9066            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9067            scan_defects: vec![ScanIssue::ScanFailure]
9068        },
9069        TelemetryEvent::ScanEvent {
9070            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9071            scan_defects: vec![ScanIssue::ScanFailure]
9072        },
9073        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9074        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9075        "Scan results no longer empty after recovery, but scan fails"
9076    )]
9077    #[test_case(
9078        TelemetryEvent::RecoveryEvent {
9079            reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::DestroyIface)
9080        },
9081        TelemetryEvent::ScanEvent {
9082            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9083            scan_defects: vec![ScanIssue::AbortedScan]
9084        },
9085        TelemetryEvent::ScanEvent {
9086            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9087            scan_defects: vec![ScanIssue::AbortedScan]
9088        },
9089        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9090        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9091        "Scan results not empty after recovery but scan is cancelled"
9092    )]
9093    #[test_case(
9094        TelemetryEvent::RecoveryEvent {
9095            reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::PhyReset)
9096        },
9097        TelemetryEvent::ScanEvent {
9098            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9099            scan_defects: vec![ScanIssue::EmptyScanResults]
9100        },
9101        TelemetryEvent::ScanEvent {
9102            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9103            scan_defects: vec![ScanIssue::EmptyScanResults]
9104        },
9105        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9106        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9107        "Scan results still empty after recovery"
9108    )]
9109    #[fuchsia::test(add_test_attr = false)]
9110    fn test_post_recovery_scan_metrics(
9111        recovery_event: TelemetryEvent,
9112        post_recovery_event: TelemetryEvent,
9113        duplicate_check_event: TelemetryEvent,
9114        expected_metric_id: u32,
9115        dimensions: Vec<u32>,
9116    ) {
9117        test_generic_post_recovery_event(
9118            recovery_event,
9119            post_recovery_event,
9120            duplicate_check_event,
9121            expected_metric_id,
9122            dimensions,
9123        );
9124    }
9125
9126    #[test_case(
9127        TelemetryEvent::RecoveryEvent {
9128            reason: RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy)
9129        },
9130        TelemetryEvent::StartApResult(Err(())),
9131        TelemetryEvent::StartApResult(Err(())),
9132        metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9133        vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::ResetPhy as u32] ;
9134        "start AP still does not work after recovery"
9135    )]
9136    #[test_case(
9137        TelemetryEvent::RecoveryEvent {
9138            reason: RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy)
9139        },
9140        TelemetryEvent::StartApResult(Ok(())),
9141        TelemetryEvent::StartApResult(Ok(())),
9142        metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9143        vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::ResetPhy as u32] ;
9144        "start AP works after recovery"
9145    )]
9146    #[fuchsia::test(add_test_attr = false)]
9147    fn test_post_recovery_start_ap(
9148        recovery_event: TelemetryEvent,
9149        post_recovery_event: TelemetryEvent,
9150        duplicate_check_event: TelemetryEvent,
9151        expected_metric_id: u32,
9152        dimensions: Vec<u32>,
9153    ) {
9154        test_generic_post_recovery_event(
9155            recovery_event,
9156            post_recovery_event,
9157            duplicate_check_event,
9158            expected_metric_id,
9159            dimensions,
9160        );
9161    }
9162
9163    #[test_case(
9164        TelemetryEvent::RecoveryEvent {
9165            reason: RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset)
9166        },
9167        TelemetryEvent::IfaceCreationResult { role: fidl_common::WlanMacRole::Client, result: Err(()) },
9168        TelemetryEvent::IfaceCreationResult { role: fidl_common::WlanMacRole::Client, result: Err(()) },
9169        metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
9170        vec![RecoveryOutcome::Failure as u32] ;
9171        "create iface still does not work after recovery"
9172    )]
9173    #[test_case(
9174        TelemetryEvent::RecoveryEvent {
9175            reason: RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset)
9176        },
9177        TelemetryEvent::IfaceCreationResult { role: fidl_common::WlanMacRole::Client, result: Ok(IFACE_ID) },
9178        TelemetryEvent::IfaceCreationResult { role: fidl_common::WlanMacRole::Client, result: Ok(IFACE_ID) },
9179        metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
9180        vec![RecoveryOutcome::Success as u32] ;
9181        "create iface works after recovery"
9182    )]
9183    #[fuchsia::test(add_test_attr = false)]
9184    fn test_post_recovery_create_iface(
9185        recovery_event: TelemetryEvent,
9186        post_recovery_event: TelemetryEvent,
9187        duplicate_check_event: TelemetryEvent,
9188        expected_metric_id: u32,
9189        dimensions: Vec<u32>,
9190    ) {
9191        test_generic_post_recovery_event(
9192            recovery_event,
9193            post_recovery_event,
9194            duplicate_check_event,
9195            expected_metric_id,
9196            dimensions,
9197        );
9198    }
9199
9200    #[test_case(
9201        TelemetryEvent::RecoveryEvent {
9202            reason: RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset)
9203        },
9204        TelemetryEvent::IfaceDestructionResult { role: fidl_common::WlanMacRole::Client, result: Err(()) },
9205        TelemetryEvent::IfaceDestructionResult { role: fidl_common::WlanMacRole::Client, result: Err(()) },
9206        metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
9207        vec![RecoveryOutcome::Failure as u32] ;
9208        "destroy iface does not work after recovery"
9209    )]
9210    #[test_case(
9211        TelemetryEvent::RecoveryEvent {
9212            reason: RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset)
9213        },
9214        TelemetryEvent::IfaceDestructionResult { role: fidl_common::WlanMacRole::Client, result: Ok(IFACE_ID) },
9215        TelemetryEvent::IfaceDestructionResult { role: fidl_common::WlanMacRole::Client, result: Ok(IFACE_ID) },
9216        metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
9217        vec![RecoveryOutcome::Success as u32] ;
9218        "destroy iface works after recovery"
9219    )]
9220    #[fuchsia::test(add_test_attr = false)]
9221    fn test_post_recovery_destroy_iface(
9222        recovery_event: TelemetryEvent,
9223        post_recovery_event: TelemetryEvent,
9224        duplicate_check_event: TelemetryEvent,
9225        expected_metric_id: u32,
9226        dimensions: Vec<u32>,
9227    ) {
9228        test_generic_post_recovery_event(
9229            recovery_event,
9230            post_recovery_event,
9231            duplicate_check_event,
9232            expected_metric_id,
9233            dimensions,
9234        );
9235    }
9236
9237    #[test_case(
9238        TelemetryEvent::RecoveryEvent {
9239            reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9240        },
9241        TelemetryEvent::ConnectResult {
9242            iface_id: IFACE_ID,
9243            policy_connect_reason: Some(
9244                client::types::ConnectReason::RetryAfterFailedConnectAttempt,
9245            ),
9246            result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
9247            multiple_bss_candidates: true,
9248            ap_state: random_bss_description!(Wpa2).into(),
9249            network_is_likely_hidden: true,
9250        },
9251        TelemetryEvent::ConnectResult {
9252            iface_id: IFACE_ID,
9253            policy_connect_reason: Some(
9254                client::types::ConnectReason::RetryAfterFailedConnectAttempt,
9255            ),
9256            result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
9257            multiple_bss_candidates: true,
9258            ap_state: random_bss_description!(Wpa2).into(),
9259            network_is_likely_hidden: true,
9260        },
9261        metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9262        vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9263        "Connect works after recovery"
9264    )]
9265    #[test_case(
9266        TelemetryEvent::RecoveryEvent {
9267            reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9268        },
9269        TelemetryEvent::Disconnected {
9270            track_subsequent_downtime: false,
9271            info: Some(fake_disconnect_info()),
9272        },
9273        TelemetryEvent::Disconnected {
9274            track_subsequent_downtime: false,
9275            info: Some(fake_disconnect_info()),
9276        },
9277        metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9278        vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9279        "Disconnect works after recovery"
9280    )]
9281    #[test_case(
9282        TelemetryEvent::RecoveryEvent {
9283            reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9284        },
9285        TelemetryEvent::StopAp { enabled_duration: zx::MonotonicDuration::from_seconds(0) },
9286        TelemetryEvent::StopAp { enabled_duration: zx::MonotonicDuration::from_seconds(0) },
9287        metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9288        vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9289        "Stop AP works after recovery"
9290    )]
9291    #[test_case(
9292        TelemetryEvent::RecoveryEvent {
9293            reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9294        },
9295        TelemetryEvent::StartApResult(Ok(())),
9296        TelemetryEvent::StartApResult(Ok(())),
9297        metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9298        vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9299        "Start AP works after recovery"
9300    )]
9301    #[test_case(
9302        TelemetryEvent::RecoveryEvent {
9303            reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::DestroyIface)
9304        },
9305        TelemetryEvent::ScanEvent {
9306            inspect_data: ScanEventInspectData::default(),
9307            scan_defects: vec![]
9308        },
9309        TelemetryEvent::ScanEvent {
9310            inspect_data: ScanEventInspectData::default(),
9311            scan_defects: vec![]
9312        },
9313        metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9314        vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::DestroyIface as u32] ;
9315        "Scan works after timeout recovery"
9316    )]
9317    #[test_case(
9318        TelemetryEvent::RecoveryEvent {
9319            reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9320        },
9321        TelemetryEvent::SmeTimeout { source: TimeoutSource::Scan },
9322        TelemetryEvent::SmeTimeout { source: TimeoutSource::Scan },
9323        metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9324        vec![RecoveryOutcome::Failure as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9325        "SME timeout after recovery"
9326    )]
9327    #[fuchsia::test(add_test_attr = false)]
9328    fn test_post_recovery_timeout(
9329        recovery_event: TelemetryEvent,
9330        post_recovery_event: TelemetryEvent,
9331        duplicate_check_event: TelemetryEvent,
9332        expected_metric_id: u32,
9333        dimensions: Vec<u32>,
9334    ) {
9335        test_generic_post_recovery_event(
9336            recovery_event,
9337            post_recovery_event,
9338            duplicate_check_event,
9339            expected_metric_id,
9340            dimensions,
9341        );
9342    }
9343
9344    #[fuchsia::test]
9345    fn test_log_scan_request_fulfillment_time() {
9346        let (mut test_helper, mut test_fut) = setup_test();
9347
9348        // Send a scan fulfillment duration
9349        let duration = zx::MonotonicDuration::from_seconds(15);
9350        test_helper.telemetry_sender.send(TelemetryEvent::ScanRequestFulfillmentTime {
9351            duration,
9352            reason: client::scan::ScanReason::ClientRequest,
9353        });
9354
9355        // Run the telemetry loop until it stalls.
9356        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9357
9358        // Expect that Cobalt has been notified of the scan fulfillment metric
9359        test_helper.drain_cobalt_events(&mut test_fut);
9360        let logged_metrics = test_helper
9361            .get_logged_metrics(metrics::SUCCESSFUL_SCAN_REQUEST_FULFILLMENT_TIME_METRIC_ID);
9362        assert_eq!(logged_metrics.len(), 1);
9363        assert_eq!(
9364            logged_metrics[0].event_codes,
9365            vec![
9366                metrics::ConnectivityWlanMetricDimensionScanFulfillmentTime::LessThanTwentyOneSeconds as u32,
9367                metrics::ConnectivityWlanMetricDimensionScanReason::ClientRequest as u32
9368            ]
9369        );
9370    }
9371
9372    #[fuchsia::test]
9373    fn test_log_scan_queue_statistics() {
9374        let (mut test_helper, mut test_fut) = setup_test();
9375
9376        // Send a scan queue report
9377        test_helper.telemetry_sender.send(TelemetryEvent::ScanQueueStatistics {
9378            fulfilled_requests: 4,
9379            remaining_requests: 12,
9380        });
9381
9382        // Run the telemetry loop until it stalls.
9383        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9384
9385        // Expect that Cobalt has been notified of the scan queue metrics
9386        test_helper.drain_cobalt_events(&mut test_fut);
9387        let logged_metrics = test_helper
9388            .get_logged_metrics(metrics::SCAN_QUEUE_STATISTICS_AFTER_COMPLETED_SCAN_METRIC_ID);
9389        assert_eq!(logged_metrics.len(), 1);
9390        assert_eq!(
9391            logged_metrics[0].event_codes,
9392            vec![
9393                metrics::ConnectivityWlanMetricDimensionScanRequestsFulfilled::Four as u32,
9394                metrics::ConnectivityWlanMetricDimensionScanRequestsRemaining::TenToFourteen as u32
9395            ]
9396        );
9397    }
9398
9399    #[fuchsia::test]
9400    fn test_log_post_connection_score_deltas_by_signal_and_post_connection_rssi_deltas() {
9401        let (mut test_helper, mut test_fut) = setup_test();
9402        let connect_time = fasync::MonotonicInstant::from_nanos(31_000_000_000);
9403
9404        let signals_deque: VecDeque<client::types::TimestampedSignal> = VecDeque::from_iter([
9405            client::types::TimestampedSignal {
9406                signal: client::types::Signal { rssi_dbm: -70, snr_db: 10 },
9407                time: connect_time + zx::MonotonicDuration::from_millis(500),
9408            },
9409            client::types::TimestampedSignal {
9410                signal: client::types::Signal { rssi_dbm: -50, snr_db: 30 },
9411                time: connect_time + zx::MonotonicDuration::from_seconds(4),
9412            },
9413            client::types::TimestampedSignal {
9414                signal: client::types::Signal { rssi_dbm: -30, snr_db: 60 },
9415                time: connect_time + zx::MonotonicDuration::from_seconds(9),
9416            },
9417            client::types::TimestampedSignal {
9418                signal: client::types::Signal { rssi_dbm: -10, snr_db: 80 },
9419                time: connect_time + zx::MonotonicDuration::from_seconds(20),
9420            },
9421        ]);
9422        let signals = HistoricalList(signals_deque);
9423        let signal_at_connect = client::types::Signal { rssi_dbm: -90, snr_db: 0 };
9424
9425        test_helper.telemetry_sender.send(TelemetryEvent::PostConnectionSignals {
9426            connect_time,
9427            signal_at_connect,
9428            signals,
9429        });
9430
9431        // Catch logged score delta metrics
9432        test_helper.drain_cobalt_events(&mut test_fut);
9433        let logged_metrics = test_helper.get_logged_metrics(
9434            metrics::AVERAGE_SCORE_DELTA_AFTER_CONNECTION_BY_INITIAL_SCORE_METRIC_ID,
9435        );
9436
9437        use metrics::AverageScoreDeltaAfterConnectionByInitialScoreMetricDimensionTimeSinceConnect as DurationDimension;
9438
9439        // Logged metrics for one, five, ten, and thirty seconds.
9440        assert_eq!(logged_metrics.len(), 4);
9441
9442        let mut prev_score = 0;
9443        // Verify one second average delta
9444        assert_eq!(logged_metrics[0].event_codes[1], DurationDimension::OneSecond as u32);
9445        assert_matches!(&logged_metrics[0].payload, MetricEventPayload::IntegerValue(delta) => {
9446            assert_gt!(*delta, prev_score);
9447            prev_score = *delta;
9448        });
9449
9450        // Verify five second average delta
9451        assert_eq!(logged_metrics[1].event_codes[1], DurationDimension::FiveSeconds as u32);
9452        assert_matches!(&logged_metrics[1].payload, MetricEventPayload::IntegerValue(delta) => {
9453            assert_gt!(*delta, prev_score);
9454            prev_score = *delta;
9455        });
9456        // Verify ten second average delta
9457        assert_eq!(logged_metrics[2].event_codes[1], DurationDimension::TenSeconds as u32);
9458        assert_matches!(&logged_metrics[2].payload, MetricEventPayload::IntegerValue(delta) => {
9459            assert_gt!(*delta, prev_score);
9460            prev_score = *delta;
9461        });
9462        // Verify thirty second average delta
9463        assert_eq!(logged_metrics[3].event_codes[1], DurationDimension::ThirtySeconds as u32);
9464        assert_matches!(&logged_metrics[3].payload, MetricEventPayload::IntegerValue(delta) => {
9465            assert_gt!(*delta, prev_score);
9466        });
9467
9468        // Catch logged RSSI delta metrics
9469        test_helper.drain_cobalt_events(&mut test_fut);
9470        let logged_metrics = test_helper.get_logged_metrics(
9471            metrics::AVERAGE_RSSI_DELTA_AFTER_CONNECTION_BY_INITIAL_RSSI_METRIC_ID,
9472        );
9473        // Logged metrics for one, five, ten, and thirty seconds.
9474        assert_eq!(logged_metrics.len(), 4);
9475
9476        // Verify one second average RSSI delta
9477        assert_eq!(logged_metrics[0].event_codes[1], DurationDimension::OneSecond as u32);
9478        assert_matches!(&logged_metrics[0].payload, MetricEventPayload::IntegerValue(delta) => {
9479            assert_eq!(*delta, 10);
9480        });
9481
9482        // Verify five second average RSSI delta
9483        assert_eq!(logged_metrics[1].event_codes[1], DurationDimension::FiveSeconds as u32);
9484        assert_matches!(&logged_metrics[1].payload, MetricEventPayload::IntegerValue(delta) => {
9485            assert_eq!(*delta, 20);
9486        });
9487        // Verify ten second average RSSI delta
9488        assert_eq!(logged_metrics[2].event_codes[1], DurationDimension::TenSeconds as u32);
9489        assert_matches!(&logged_metrics[2].payload, MetricEventPayload::IntegerValue(delta) => {
9490            assert_eq!(*delta, 30);
9491        });
9492        // Verify thirty second average RSSI delta
9493        assert_eq!(logged_metrics[3].event_codes[1], DurationDimension::ThirtySeconds as u32);
9494        assert_matches!(&logged_metrics[3].payload, MetricEventPayload::IntegerValue(delta) => {
9495            assert_eq!(*delta, 40);
9496        });
9497    }
9498
9499    #[fuchsia::test]
9500    fn test_log_pre_disconnect_score_deltas_by_signal_and_pre_disconnect_rssi_deltas() {
9501        let (mut test_helper, mut test_fut) = setup_test();
9502        // 31 seconds
9503        let final_score_time = fasync::MonotonicInstant::from_nanos(31_000_000_000);
9504
9505        let signals_deque: VecDeque<client::types::TimestampedSignal> = VecDeque::from_iter([
9506            client::types::TimestampedSignal {
9507                signal: client::types::Signal { rssi_dbm: -10, snr_db: 80 },
9508                time: final_score_time - zx::MonotonicDuration::from_seconds(20),
9509            },
9510            client::types::TimestampedSignal {
9511                signal: client::types::Signal { rssi_dbm: -30, snr_db: 60 },
9512                time: final_score_time - zx::MonotonicDuration::from_seconds(9),
9513            },
9514            client::types::TimestampedSignal {
9515                signal: client::types::Signal { rssi_dbm: -50, snr_db: 30 },
9516                time: final_score_time - zx::MonotonicDuration::from_seconds(4),
9517            },
9518            client::types::TimestampedSignal {
9519                signal: client::types::Signal { rssi_dbm: -70, snr_db: 10 },
9520                time: final_score_time - zx::MonotonicDuration::from_millis(500),
9521            },
9522            client::types::TimestampedSignal {
9523                signal: client::types::Signal { rssi_dbm: -90, snr_db: 0 },
9524                time: final_score_time,
9525            },
9526        ]);
9527        let signals = HistoricalList(signals_deque);
9528
9529        let disconnect_info = DisconnectInfo {
9530            connected_duration: AVERAGE_SCORE_DELTA_MINIMUM_DURATION,
9531            signals,
9532            ..fake_disconnect_info()
9533        };
9534        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
9535            track_subsequent_downtime: false,
9536            info: Some(disconnect_info),
9537        });
9538
9539        // Catch logged score delta metrics
9540        test_helper.drain_cobalt_events(&mut test_fut);
9541        let logged_metrics = test_helper.get_logged_metrics(
9542            metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
9543        );
9544
9545        use metrics::AverageScoreDeltaBeforeDisconnectByFinalScoreMetricDimensionTimeUntilDisconnect as DurationDimension;
9546
9547        // Logged metrics for one, five, ten, and thirty seconds.
9548        assert_eq!(logged_metrics.len(), 4);
9549
9550        let mut prev_score = 0;
9551        // Verify one second average delta
9552        assert_eq!(logged_metrics[0].event_codes[1], DurationDimension::OneSecond as u32);
9553        assert_matches!(&logged_metrics[0].payload, MetricEventPayload::IntegerValue(delta) => {
9554            assert_gt!(*delta, prev_score);
9555            prev_score = *delta;
9556        });
9557
9558        // Verify five second average delta
9559        assert_eq!(logged_metrics[1].event_codes[1], DurationDimension::FiveSeconds as u32);
9560        assert_matches!(&logged_metrics[1].payload, MetricEventPayload::IntegerValue(delta) => {
9561            assert_gt!(*delta, prev_score);
9562            prev_score = *delta;
9563        });
9564        // Verify ten second average delta
9565        assert_eq!(logged_metrics[2].event_codes[1], DurationDimension::TenSeconds as u32);
9566        assert_matches!(&logged_metrics[2].payload, MetricEventPayload::IntegerValue(delta) => {
9567            assert_gt!(*delta, prev_score);
9568            prev_score = *delta;
9569        });
9570        // Verify thirty second average delta
9571        assert_eq!(logged_metrics[3].event_codes[1], DurationDimension::ThirtySeconds as u32);
9572        assert_matches!(&logged_metrics[3].payload, MetricEventPayload::IntegerValue(delta) => {
9573            assert_gt!(*delta, prev_score);
9574        });
9575
9576        // Catch logged RSSI delta metrics
9577        test_helper.drain_cobalt_events(&mut test_fut);
9578        let logged_metrics = test_helper.get_logged_metrics(
9579            metrics::AVERAGE_RSSI_DELTA_BEFORE_DISCONNECT_BY_FINAL_RSSI_METRIC_ID,
9580        );
9581        // Logged metrics for one, five, ten, and thirty seconds.
9582        assert_eq!(logged_metrics.len(), 4);
9583
9584        // Verify one second average RSSI delta
9585        assert_eq!(logged_metrics[0].event_codes[1], DurationDimension::OneSecond as u32);
9586        assert_matches!(&logged_metrics[0].payload, MetricEventPayload::IntegerValue(delta) => {
9587            assert_eq!(*delta, 10);
9588        });
9589
9590        // Verify five second average RSSI delta
9591        assert_eq!(logged_metrics[1].event_codes[1], DurationDimension::FiveSeconds as u32);
9592        assert_matches!(&logged_metrics[1].payload, MetricEventPayload::IntegerValue(delta) => {
9593            assert_eq!(*delta, 20);
9594        });
9595        // Verify ten second average RSSI delta
9596        assert_eq!(logged_metrics[2].event_codes[1], DurationDimension::TenSeconds as u32);
9597        assert_matches!(&logged_metrics[2].payload, MetricEventPayload::IntegerValue(delta) => {
9598            assert_eq!(*delta, 30);
9599        });
9600        // Verify thirty second average RSSI delta
9601        assert_eq!(logged_metrics[3].event_codes[1], DurationDimension::ThirtySeconds as u32);
9602        assert_matches!(&logged_metrics[3].payload, MetricEventPayload::IntegerValue(delta) => {
9603            assert_eq!(*delta, 40);
9604        });
9605
9606        // Record a disconnect shorter than the minimum required duration
9607        let disconnect_info = DisconnectInfo {
9608            connected_duration: AVERAGE_SCORE_DELTA_MINIMUM_DURATION
9609                - zx::MonotonicDuration::from_seconds(1),
9610            ..fake_disconnect_info()
9611        };
9612        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
9613            track_subsequent_downtime: false,
9614            info: Some(disconnect_info),
9615        });
9616        test_helper.drain_cobalt_events(&mut test_fut);
9617
9618        // No additional metrics should be logged.
9619        let logged_metrics = test_helper.get_logged_metrics(
9620            metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
9621        );
9622        assert_eq!(logged_metrics.len(), 4);
9623        let logged_metrics = test_helper.get_logged_metrics(
9624            metrics::AVERAGE_RSSI_DELTA_BEFORE_DISCONNECT_BY_FINAL_RSSI_METRIC_ID,
9625        );
9626        assert_eq!(logged_metrics.len(), 4);
9627    }
9628
9629    #[fuchsia::test]
9630    fn test_log_network_selection_metrics() {
9631        let (mut test_helper, mut test_fut) = setup_test();
9632
9633        // Send network selection event
9634        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
9635            network_selection_type: NetworkSelectionType::Undirected,
9636            num_candidates: Ok(3),
9637            selected_count: 2,
9638        });
9639
9640        // Run the telemetry loop until it stalls.
9641        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9642        test_helper.drain_cobalt_events(&mut test_fut);
9643
9644        // Verify the network selection is counted
9645        let logged_metrics =
9646            test_helper.get_logged_metrics(metrics::NETWORK_SELECTION_COUNT_METRIC_ID);
9647        assert_eq!(logged_metrics.len(), 1);
9648        assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
9649
9650        // Verify the number of selected candidates is recorded
9651        let logged_metrics =
9652            test_helper.get_logged_metrics(metrics::NUM_NETWORKS_SELECTED_METRIC_ID);
9653        assert_eq!(logged_metrics.len(), 1);
9654        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(2));
9655
9656        // Send a network selection metric where there were 0 candidates.
9657        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
9658            network_selection_type: NetworkSelectionType::Undirected,
9659            num_candidates: Ok(0),
9660            selected_count: 0,
9661        });
9662
9663        // Run the telemetry loop until it stalls.
9664        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9665        test_helper.drain_cobalt_events(&mut test_fut);
9666
9667        // Verify the network selection is counted
9668        let logged_metrics =
9669            test_helper.get_logged_metrics(metrics::NETWORK_SELECTION_COUNT_METRIC_ID);
9670        assert_eq!(logged_metrics.len(), 2);
9671
9672        // The number of selected networks should not be recorded, since there were no candidates
9673        // to select from
9674        let logged_metrics =
9675            test_helper.get_logged_metrics(metrics::NUM_NETWORKS_SELECTED_METRIC_ID);
9676        assert_eq!(logged_metrics.len(), 1);
9677    }
9678
9679    #[fuchsia::test]
9680    fn test_log_bss_selection_metrics() {
9681        let (mut test_helper, mut test_fut) = setup_test();
9682
9683        let selected_candidate_2g = client::types::ScannedCandidate {
9684            bss: client::types::Bss {
9685                channel: Channel::new(1, wlan_common::channel::Bandwidth::Cbw20, TwoGhz),
9686                ..generate_random_bss()
9687            },
9688            ..generate_random_scanned_candidate()
9689        };
9690        let candidate_2g = client::types::ScannedCandidate {
9691            bss: client::types::Bss {
9692                channel: Channel::new(1, wlan_common::channel::Bandwidth::Cbw20, TwoGhz),
9693                ..generate_random_bss()
9694            },
9695            ..generate_random_scanned_candidate()
9696        };
9697        let candidate_5g = client::types::ScannedCandidate {
9698            bss: client::types::Bss {
9699                channel: Channel::new(36, wlan_common::channel::Bandwidth::Cbw40, FiveGhz),
9700                ..generate_random_bss()
9701            },
9702            ..generate_random_scanned_candidate()
9703        };
9704        let scored_candidates =
9705            vec![(selected_candidate_2g.clone(), 70), (candidate_2g, 60), (candidate_5g, 50)];
9706
9707        test_helper.telemetry_sender.send(TelemetryEvent::BssSelectionResult {
9708            reason: client::types::ConnectReason::FidlConnectRequest,
9709            scored_candidates: scored_candidates.clone(),
9710            selected_candidate: Some((selected_candidate_2g, 70)),
9711        });
9712
9713        test_helper.drain_cobalt_events(&mut test_fut);
9714
9715        let fidl_connect_event_code = vec![
9716            metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::FidlConnectRequest
9717                as u32,
9718        ];
9719        // Check that the BSS selection occurrence metrics are logged
9720        let logged_metrics = test_helper.get_logged_metrics(metrics::BSS_SELECTION_COUNT_METRIC_ID);
9721        assert_eq!(logged_metrics.len(), 1);
9722        assert_eq!(logged_metrics[0].event_codes, Vec::<u32>::new());
9723        assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
9724
9725        let logged_metrics =
9726            test_helper.get_logged_metrics(metrics::BSS_SELECTION_COUNT_DETAILED_METRIC_ID);
9727        assert_eq!(logged_metrics.len(), 1);
9728        assert_eq!(logged_metrics[0].event_codes, fidl_connect_event_code);
9729        assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
9730
9731        // Check that the candidate count metrics are logged
9732        let logged_metrics =
9733            test_helper.get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_METRIC_ID);
9734        assert_eq!(logged_metrics.len(), 1);
9735        assert_eq!(logged_metrics[0].event_codes, Vec::<u32>::new());
9736        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(3));
9737
9738        let logged_metrics = test_helper
9739            .get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_DETAILED_METRIC_ID);
9740        assert_eq!(logged_metrics.len(), 1);
9741        assert_eq!(logged_metrics[0].event_codes, fidl_connect_event_code);
9742        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(3));
9743
9744        // Check that all candidate scores are logged
9745        let logged_metrics = test_helper.get_logged_metrics(metrics::BSS_CANDIDATE_SCORE_METRIC_ID);
9746        assert_eq!(logged_metrics.len(), 3);
9747        for i in 0..3 {
9748            assert_eq!(
9749                logged_metrics[i].payload,
9750                MetricEventPayload::IntegerValue(scored_candidates[i].1 as i64)
9751            )
9752        }
9753
9754        // Check that unique network count is logged
9755        let logged_metrics = test_helper
9756            .get_logged_metrics(metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID);
9757        assert_eq!(logged_metrics.len(), 1);
9758        assert_eq!(logged_metrics[0].event_codes, fidl_connect_event_code);
9759        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(3));
9760
9761        // Check that selected candidate score is logged
9762        let logged_metrics = test_helper.get_logged_metrics(metrics::SELECTED_BSS_SCORE_METRIC_ID);
9763        assert_eq!(logged_metrics.len(), 1);
9764        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(70));
9765
9766        // Check that runner-up score delta is logged
9767        let logged_metrics =
9768            test_helper.get_logged_metrics(metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID);
9769        assert_eq!(logged_metrics.len(), 1);
9770        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(10));
9771
9772        // Check that GHz score delta is logged
9773        let logged_metrics =
9774            test_helper.get_logged_metrics(metrics::BEST_CANDIDATES_GHZ_SCORE_DELTA_METRIC_ID);
9775        assert_eq!(logged_metrics.len(), 1);
9776        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(-20));
9777
9778        // Check that GHz bands present in selection is logged
9779        let logged_metrics =
9780            test_helper.get_logged_metrics(metrics::GHZ_BANDS_AVAILABLE_IN_BSS_SELECTION_METRIC_ID);
9781        assert_eq!(logged_metrics.len(), 1);
9782        assert_eq!(
9783            logged_metrics[0].event_codes,
9784            vec![metrics::GhzBandsAvailableInBssSelectionMetricDimensionBands::MultiBand as u32]
9785        );
9786        assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
9787    }
9788
9789    #[fuchsia::test]
9790    fn test_log_bss_selection_metrics_none_selected() {
9791        let (mut test_helper, mut test_fut) = setup_test();
9792
9793        test_helper.telemetry_sender.send(TelemetryEvent::BssSelectionResult {
9794            reason: client::types::ConnectReason::FidlConnectRequest,
9795            scored_candidates: vec![],
9796            selected_candidate: None,
9797        });
9798
9799        test_helper.drain_cobalt_events(&mut test_fut);
9800
9801        // Check that only the BSS selection occurrence and candidate count metrics are recorded
9802        assert!(!test_helper.get_logged_metrics(metrics::BSS_SELECTION_COUNT_METRIC_ID).is_empty());
9803        assert!(
9804            !test_helper
9805                .get_logged_metrics(metrics::BSS_SELECTION_COUNT_DETAILED_METRIC_ID)
9806                .is_empty()
9807        );
9808        assert!(
9809            !test_helper
9810                .get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_METRIC_ID)
9811                .is_empty()
9812        );
9813        assert!(
9814            !test_helper
9815                .get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_DETAILED_METRIC_ID)
9816                .is_empty()
9817        );
9818        assert!(test_helper.get_logged_metrics(metrics::BSS_CANDIDATE_SCORE_METRIC_ID).is_empty());
9819        assert!(
9820            test_helper
9821                .get_logged_metrics(metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID)
9822                .is_empty()
9823        );
9824        assert!(
9825            test_helper
9826                .get_logged_metrics(metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID)
9827                .is_empty()
9828        );
9829        assert!(
9830            test_helper
9831                .get_logged_metrics(metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID)
9832                .is_empty()
9833        );
9834        assert!(
9835            test_helper
9836                .get_logged_metrics(metrics::BEST_CANDIDATES_GHZ_SCORE_DELTA_METRIC_ID)
9837                .is_empty()
9838        );
9839        assert!(
9840            test_helper
9841                .get_logged_metrics(metrics::GHZ_BANDS_AVAILABLE_IN_BSS_SELECTION_METRIC_ID)
9842                .is_empty()
9843        );
9844    }
9845
9846    #[fuchsia::test]
9847    fn test_log_bss_selection_metrics_runner_up_delta_not_recorded() {
9848        let (mut test_helper, mut test_fut) = setup_test();
9849
9850        let scored_candidates = vec![
9851            (generate_random_scanned_candidate(), 90),
9852            (generate_random_scanned_candidate(), 60),
9853            (generate_random_scanned_candidate(), 50),
9854        ];
9855
9856        test_helper.telemetry_sender.send(TelemetryEvent::BssSelectionResult {
9857            reason: client::types::ConnectReason::FidlConnectRequest,
9858            scored_candidates,
9859            // Report that the selected candidate was not the highest scoring candidate.
9860            selected_candidate: Some((generate_random_scanned_candidate(), 60)),
9861        });
9862
9863        test_helper.drain_cobalt_events(&mut test_fut);
9864
9865        // No delta metric should be recorded
9866        assert!(
9867            test_helper
9868                .get_logged_metrics(metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID)
9869                .is_empty()
9870        );
9871    }
9872
9873    #[fuchsia::test]
9874    fn test_log_connection_score_average_long_duration() {
9875        let (mut test_helper, mut test_fut) = setup_test();
9876        let now = fasync::MonotonicInstant::now();
9877        let signals = vec![
9878            client::types::TimestampedSignal {
9879                signal: client::types::Signal { rssi_dbm: -60, snr_db: 30 },
9880                time: now,
9881            },
9882            client::types::TimestampedSignal {
9883                signal: client::types::Signal { rssi_dbm: -60, snr_db: 30 },
9884                time: now,
9885            },
9886            client::types::TimestampedSignal {
9887                signal: client::types::Signal { rssi_dbm: -80, snr_db: 10 },
9888                time: now,
9889            },
9890            client::types::TimestampedSignal {
9891                signal: client::types::Signal { rssi_dbm: -80, snr_db: 10 },
9892                time: now,
9893            },
9894        ];
9895
9896        test_helper.telemetry_sender.send(TelemetryEvent::LongDurationSignals { signals });
9897        test_helper.drain_cobalt_events(&mut test_fut);
9898
9899        let logged_metrics =
9900            test_helper.get_logged_metrics(metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID);
9901        assert_eq!(logged_metrics.len(), 1);
9902        assert_eq!(
9903            logged_metrics[0].event_codes,
9904            vec![metrics::ConnectionScoreAverageMetricDimensionDuration::LongDuration as u32]
9905        );
9906        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(55));
9907
9908        // Ensure an empty score list would not cause an arithmetic error.
9909        test_helper.telemetry_sender.send(TelemetryEvent::LongDurationSignals { signals: vec![] });
9910        test_helper.drain_cobalt_events(&mut test_fut);
9911        assert_eq!(
9912            test_helper.get_logged_metrics(metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID).len(),
9913            1
9914        );
9915    }
9916
9917    #[fuchsia::test]
9918    fn test_log_connection_rssi_average_long_duration() {
9919        let (mut test_helper, mut test_fut) = setup_test();
9920        let now = fasync::MonotonicInstant::now();
9921        let signals = vec![
9922            client::types::TimestampedSignal {
9923                signal: client::types::Signal { rssi_dbm: -60, snr_db: 30 },
9924                time: now,
9925            },
9926            client::types::TimestampedSignal {
9927                signal: client::types::Signal { rssi_dbm: -60, snr_db: 30 },
9928                time: now,
9929            },
9930            client::types::TimestampedSignal {
9931                signal: client::types::Signal { rssi_dbm: -80, snr_db: 10 },
9932                time: now,
9933            },
9934            client::types::TimestampedSignal {
9935                signal: client::types::Signal { rssi_dbm: -80, snr_db: 10 },
9936                time: now,
9937            },
9938        ];
9939
9940        test_helper.telemetry_sender.send(TelemetryEvent::LongDurationSignals { signals });
9941        test_helper.drain_cobalt_events(&mut test_fut);
9942
9943        let logged_metrics =
9944            test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_AVERAGE_METRIC_ID);
9945        assert_eq!(logged_metrics.len(), 1);
9946        assert_eq!(
9947            logged_metrics[0].event_codes,
9948            vec![metrics::ConnectionScoreAverageMetricDimensionDuration::LongDuration as u32]
9949        );
9950        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(-70));
9951
9952        // Ensure an empty score list would not cause an arithmetic error.
9953        test_helper.telemetry_sender.send(TelemetryEvent::LongDurationSignals { signals: vec![] });
9954        test_helper.drain_cobalt_events(&mut test_fut);
9955        assert_eq!(
9956            test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_AVERAGE_METRIC_ID).len(),
9957            1
9958        );
9959    }
9960
9961    struct TestHelper {
9962        telemetry_sender: TelemetrySender,
9963        inspector: Inspector,
9964        monitor_svc_stream: fidl_fuchsia_wlan_device_service::DeviceMonitorRequestStream,
9965        telemetry_svc_streams: Vec<fidl_fuchsia_wlan_sme::TelemetryRequestStream>,
9966        cobalt_stream: fidl_fuchsia_metrics::MetricEventLoggerRequestStream,
9967        iface_stats_resp:
9968            Option<Box<dyn Fn() -> fidl_fuchsia_wlan_sme::TelemetryGetIfaceStatsResult>>,
9969        /// As requests to Cobalt are responded to via `self.drain_cobalt_events()`,
9970        /// their payloads are drained to this HashMap
9971        cobalt_events: Vec<MetricEvent>,
9972        _defect_receiver: mpsc::Receiver<Defect>,
9973
9974        // Note: keep the executor field last in the struct so it gets dropped last.
9975        exec: fasync::TestExecutor,
9976    }
9977
9978    impl TestHelper {
9979        /// Advance executor until stalled.
9980        /// This function will also reply to any ongoing requests to establish an iface
9981        /// telemetry channel.
9982        fn advance_test_fut<T>(
9983            &mut self,
9984            test_fut: &mut (impl Future<Output = T> + Unpin),
9985        ) -> Poll<T> {
9986            let mut result = self.exec.run_until_stalled(test_fut);
9987            while let Poll::Ready(Some(Ok(req))) =
9988                self.exec.run_until_stalled(&mut self.monitor_svc_stream.next())
9989            {
9990                match req {
9991                    fidl_fuchsia_wlan_device_service::DeviceMonitorRequest::GetSmeTelemetry {
9992                        iface_id,
9993                        telemetry_server,
9994                        responder,
9995                    } => {
9996                        assert_eq!(iface_id, IFACE_ID);
9997                        let telemetry_stream = telemetry_server.into_stream();
9998                        responder.send(Ok(())).expect("Failed to respond to telemetry request");
9999                        self.telemetry_svc_streams.push(telemetry_stream);
10000                        result = self.exec.run_until_stalled(test_fut);
10001                    }
10002                    _ => panic!("Unexpected device monitor request: {req:?}"),
10003                }
10004            }
10005            result
10006        }
10007
10008        /// Advance executor by `duration`.
10009        /// This function dynamically jumps fake time to the next scheduled timer's deadline
10010        /// (or target time if no intermediate timers exist), triggering expired timers and
10011        /// running the test_fut, until `duration` is reached.
10012        fn advance_by(
10013            &mut self,
10014            duration: zx::MonotonicDuration,
10015            mut test_fut: Pin<&mut impl Future<Output = ()>>,
10016        ) {
10017            let target_time = self.exec.now() + duration;
10018
10019            // When using large time jumps, we must poll the executor once at the current virtual
10020            // time BEFORE advancing the clock. Otherwise, any pending events in channels (which
10021            // were sent just before this call) will not be processed until AFTER the clock has
10022            // jumped. The service would then timestamp these events at the post-jump virtual time,
10023            // incorrectly shifting them into the future and breaking duration-based assertions.
10024            // This poll flushes those pending events at their correct virtual arrival time and
10025            // registers any new timers they might schedule.
10026            assert_eq!(self.advance_test_fut(&mut test_fut), Poll::Pending);
10027            self.telemetry_svc_streams.retain(|s| !s.is_terminated());
10028            for telemetry_svc_stream in &mut self.telemetry_svc_streams {
10029                respond_iface_counter_stats_req(
10030                    &mut self.exec,
10031                    telemetry_svc_stream,
10032                    &self.iface_stats_resp,
10033                );
10034            }
10035            self.drain_cobalt_events(&mut test_fut);
10036            assert_eq!(self.advance_test_fut(&mut test_fut), Poll::Pending);
10037
10038            while self.exec.now() < target_time {
10039                let next_timer = fasync::TestExecutor::next_timer();
10040                let next_wake = match next_timer {
10041                    Some(time) if time <= target_time => time,
10042                    _ => target_time,
10043                };
10044
10045                if next_wake > self.exec.now() {
10046                    self.exec.set_fake_time(next_wake);
10047                }
10048
10049                let _ = self.exec.wake_expired_timers();
10050                assert_eq!(self.advance_test_fut(&mut test_fut), Poll::Pending);
10051
10052                self.telemetry_svc_streams.retain(|s| !s.is_terminated());
10053                for telemetry_svc_stream in &mut self.telemetry_svc_streams {
10054                    respond_iface_counter_stats_req(
10055                        &mut self.exec,
10056                        telemetry_svc_stream,
10057                        &self.iface_stats_resp,
10058                    );
10059                }
10060
10061                // Respond to any potential Cobalt request, draining their payloads to
10062                // `self.cobalt_events`.
10063                self.drain_cobalt_events(&mut test_fut);
10064
10065                assert_eq!(self.advance_test_fut(&mut test_fut), Poll::Pending);
10066            }
10067        }
10068
10069        fn set_iface_stats_resp(
10070            &mut self,
10071            iface_stats_resp: Box<dyn Fn() -> fidl_fuchsia_wlan_sme::TelemetryGetIfaceStatsResult>,
10072        ) {
10073            let _ = self.iface_stats_resp.replace(iface_stats_resp);
10074        }
10075
10076        /// Advance executor by some duration until the next time `test_fut` handles periodic
10077        /// telemetry. This uses `self.advance_by` underneath.
10078        ///
10079        /// This function assumes that executor starts test_fut at time 0 (which should be true
10080        /// if TestHelper is created from `setup_test()`)
10081        fn advance_to_next_telemetry_checkpoint(
10082            &mut self,
10083            test_fut: Pin<&mut impl Future<Output = ()>>,
10084        ) {
10085            let now = fasync::MonotonicInstant::now();
10086            let remaining_interval = TELEMETRY_QUERY_INTERVAL.into_nanos()
10087                - (now.into_nanos() % TELEMETRY_QUERY_INTERVAL.into_nanos());
10088            self.advance_by(zx::MonotonicDuration::from_nanos(remaining_interval), test_fut)
10089        }
10090
10091        /// Continually execute the future and respond to any incoming Cobalt request with Ok.
10092        /// Append each metric request payload into `self.cobalt_events`.
10093        fn drain_cobalt_events(&mut self, test_fut: &mut (impl Future + Unpin)) {
10094            let mut made_progress = true;
10095            while made_progress {
10096                let _result = self.advance_test_fut(test_fut);
10097                made_progress = false;
10098                while let Poll::Ready(Some(Ok(req))) =
10099                    self.exec.run_until_stalled(&mut self.cobalt_stream.next())
10100                {
10101                    self.cobalt_events.append(&mut req.respond_to_metric_req(Ok(())));
10102                    made_progress = true;
10103                }
10104            }
10105        }
10106
10107        fn get_logged_metrics(&self, metric_id: u32) -> Vec<MetricEvent> {
10108            self.cobalt_events.iter().filter(|ev| ev.metric_id == metric_id).cloned().collect()
10109        }
10110
10111        fn send_connected_event(&mut self, ap_state: impl Into<client::types::ApState>) {
10112            let event = TelemetryEvent::ConnectResult {
10113                iface_id: IFACE_ID,
10114                policy_connect_reason: Some(
10115                    client::types::ConnectReason::RetryAfterFailedConnectAttempt,
10116                ),
10117                result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
10118                multiple_bss_candidates: true,
10119                ap_state: ap_state.into(),
10120                network_is_likely_hidden: true,
10121            };
10122            self.telemetry_sender.send(event);
10123        }
10124
10125        // Empty the cobalt metrics can be stored so that future checks on cobalt metrics can
10126        // ignore previous values.
10127        fn clear_cobalt_events(&mut self) {
10128            self.cobalt_events = Vec::new();
10129        }
10130    }
10131
10132    fn respond_iface_counter_stats_req(
10133        executor: &mut fasync::TestExecutor,
10134        telemetry_svc_stream: &mut fidl_fuchsia_wlan_sme::TelemetryRequestStream,
10135        iface_stats_resp: &Option<
10136            Box<dyn Fn() -> fidl_fuchsia_wlan_sme::TelemetryGetIfaceStatsResult>,
10137        >,
10138    ) {
10139        while let Poll::Ready(Ok(Some(request))) =
10140            executor.run_until_stalled(&mut pin!(telemetry_svc_stream.try_next()))
10141        {
10142            match request {
10143                fidl_fuchsia_wlan_sme::TelemetryRequest::GetIfaceStats { responder } => {
10144                    let resp = match &iface_stats_resp {
10145                        Some(get_resp) => get_resp(),
10146                        None => {
10147                            let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
10148                            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
10149                                connection_stats: Some(fake_connection_stats(seed)),
10150                                ..Default::default()
10151                            })
10152                        }
10153                    };
10154                    let _ = responder.send(resp.as_ref().map_err(|e| *e));
10155                    break;
10156                }
10157                fidl_fuchsia_wlan_sme::TelemetryRequest::QueryTelemetrySupport { responder } => {
10158                    let _ = responder.send(Ok(&Default::default()));
10159                }
10160                _ => {
10161                    panic!("unexpected request: {request:?}");
10162                }
10163            }
10164        }
10165    }
10166
10167    fn respond_iface_histogram_stats_req(
10168        executor: &mut fasync::TestExecutor,
10169        telemetry_svc_stream: &mut fidl_fuchsia_wlan_sme::TelemetryRequestStream,
10170    ) {
10171        while let Poll::Ready(Ok(Some(request))) =
10172            executor.run_until_stalled(&mut pin!(telemetry_svc_stream.try_next()))
10173        {
10174            match request {
10175                fidl_fuchsia_wlan_sme::TelemetryRequest::GetHistogramStats { responder } => {
10176                    let _ = responder.send(Ok(&fake_iface_histogram_stats()));
10177                    break;
10178                }
10179                fidl_fuchsia_wlan_sme::TelemetryRequest::GetIfaceStats { responder } => {
10180                    let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
10181                    let stats = fidl_fuchsia_wlan_stats::IfaceStats {
10182                        connection_stats: Some(fake_connection_stats(seed)),
10183                        ..Default::default()
10184                    };
10185                    let _ = responder.send(Ok(&stats));
10186                }
10187                fidl_fuchsia_wlan_sme::TelemetryRequest::GetSignalReport { responder } => {
10188                    let _ = responder.send(Ok(&fidl_fuchsia_wlan_stats::SignalReport::default()));
10189                }
10190                fidl_fuchsia_wlan_sme::TelemetryRequest::QueryTelemetrySupport { responder } => {
10191                    let _ = responder.send(Ok(&Default::default()));
10192                }
10193                _ => {
10194                    panic!("unexpected request: {request:?}");
10195                }
10196            }
10197        }
10198    }
10199
10200    /// Assert two set of Cobalt MetricEvent equal, disregarding the order
10201    #[track_caller]
10202    fn assert_eq_cobalt_events(
10203        mut left: Vec<fidl_fuchsia_metrics::MetricEvent>,
10204        mut right: Vec<fidl_fuchsia_metrics::MetricEvent>,
10205    ) {
10206        left.sort_by(metric_event_cmp);
10207        right.sort_by(metric_event_cmp);
10208        assert_eq!(left, right);
10209    }
10210
10211    fn metric_event_cmp(
10212        left: &fidl_fuchsia_metrics::MetricEvent,
10213        right: &fidl_fuchsia_metrics::MetricEvent,
10214    ) -> std::cmp::Ordering {
10215        match left.metric_id.cmp(&right.metric_id) {
10216            std::cmp::Ordering::Equal => match left.event_codes.len().cmp(&right.event_codes.len())
10217            {
10218                std::cmp::Ordering::Equal => (),
10219                ordering => return ordering,
10220            },
10221            ordering => return ordering,
10222        }
10223
10224        for i in 0..left.event_codes.len() {
10225            match left.event_codes[i].cmp(&right.event_codes[i]) {
10226                std::cmp::Ordering::Equal => (),
10227                ordering => return ordering,
10228            }
10229        }
10230
10231        match (&left.payload, &right.payload) {
10232            (MetricEventPayload::Count(v1), MetricEventPayload::Count(v2)) => v1.cmp(v2),
10233            (MetricEventPayload::IntegerValue(v1), MetricEventPayload::IntegerValue(v2)) => {
10234                v1.cmp(v2)
10235            }
10236            (MetricEventPayload::StringValue(v1), MetricEventPayload::StringValue(v2)) => {
10237                v1.cmp(v2)
10238            }
10239            (MetricEventPayload::Histogram(_), MetricEventPayload::Histogram(_)) => {
10240                unimplemented!()
10241            }
10242            _ => unimplemented!(),
10243        }
10244    }
10245
10246    trait CobaltExt {
10247        // Respond to MetricEventLoggerRequest and extract its MetricEvent
10248        fn respond_to_metric_req(
10249            self,
10250            result: Result<(), fidl_fuchsia_metrics::Error>,
10251        ) -> Vec<fidl_fuchsia_metrics::MetricEvent>;
10252    }
10253
10254    impl CobaltExt for MetricEventLoggerRequest {
10255        fn respond_to_metric_req(
10256            self,
10257            result: Result<(), fidl_fuchsia_metrics::Error>,
10258        ) -> Vec<fidl_fuchsia_metrics::MetricEvent> {
10259            match self {
10260                Self::LogOccurrence { metric_id, count, event_codes, responder } => {
10261                    assert!(responder.send(result).is_ok());
10262                    vec![MetricEvent {
10263                        metric_id,
10264                        event_codes,
10265                        payload: MetricEventPayload::Count(count),
10266                    }]
10267                }
10268                Self::LogInteger { metric_id, value, event_codes, responder } => {
10269                    assert!(responder.send(result).is_ok());
10270                    vec![MetricEvent {
10271                        metric_id,
10272                        event_codes,
10273                        payload: MetricEventPayload::IntegerValue(value),
10274                    }]
10275                }
10276                Self::LogIntegerHistogram { metric_id, histogram, event_codes, responder } => {
10277                    assert!(responder.send(result).is_ok());
10278                    vec![MetricEvent {
10279                        metric_id,
10280                        event_codes,
10281                        payload: MetricEventPayload::Histogram(histogram),
10282                    }]
10283                }
10284                Self::LogString { metric_id, string_value, event_codes, responder } => {
10285                    assert!(responder.send(result).is_ok());
10286                    vec![MetricEvent {
10287                        metric_id,
10288                        event_codes,
10289                        payload: MetricEventPayload::StringValue(string_value),
10290                    }]
10291                }
10292                Self::LogMetricEvents { events, responder } => {
10293                    assert!(responder.send(result).is_ok());
10294                    events
10295                }
10296            }
10297        }
10298    }
10299
10300    fn setup_test() -> (TestHelper, Pin<Box<impl Future<Output = ()>>>) {
10301        let mut exec = fasync::TestExecutor::new_with_fake_time();
10302        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
10303
10304        let (monitor_svc_proxy, monitor_svc_stream) =
10305            create_proxy_and_stream::<fidl_fuchsia_wlan_device_service::DeviceMonitorMarker>();
10306
10307        let (cobalt_proxy, cobalt_stream) =
10308            create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
10309
10310        let inspector = Inspector::default();
10311        let inspect_node = inspector.root().create_child("stats");
10312        let external_inspect_node = inspector.root().create_child("external");
10313        let (defect_sender, _defect_receiver) = mpsc::channel(100);
10314        let (telemetry_sender, test_fut) = serve_telemetry(
10315            monitor_svc_proxy,
10316            cobalt_proxy.clone(),
10317            inspect_node,
10318            external_inspect_node.create_child("stats"),
10319            defect_sender,
10320        );
10321        inspector.root().record(external_inspect_node);
10322        let mut test_fut = Box::pin(test_fut);
10323
10324        assert_eq!(exec.run_until_stalled(&mut test_fut), Poll::Pending);
10325
10326        let test_helper = TestHelper {
10327            telemetry_sender,
10328            inspector,
10329            monitor_svc_stream,
10330            telemetry_svc_streams: vec![],
10331            cobalt_stream,
10332            iface_stats_resp: None,
10333            cobalt_events: vec![],
10334            _defect_receiver,
10335            exec,
10336        };
10337        (test_helper, test_fut)
10338    }
10339
10340    fn fake_connection_stats(nth_req: u64) -> fidl_fuchsia_wlan_stats::ConnectionStats {
10341        fidl_fuchsia_wlan_stats::ConnectionStats {
10342            connection_id: Some(1),
10343            rx_unicast_total: Some(nth_req),
10344            rx_unicast_drop: Some(0),
10345            rx_multicast: Some(2 * nth_req),
10346            tx_total: Some(nth_req),
10347            tx_drop: Some(0),
10348            ..Default::default()
10349        }
10350    }
10351
10352    fn fake_iface_histogram_stats() -> fidl_fuchsia_wlan_stats::IfaceHistogramStats {
10353        fidl_fuchsia_wlan_stats::IfaceHistogramStats {
10354            noise_floor_histograms: Some(fake_noise_floor_histograms()),
10355            rssi_histograms: Some(fake_rssi_histograms()),
10356            rx_rate_index_histograms: Some(fake_rx_rate_index_histograms()),
10357            snr_histograms: Some(fake_snr_histograms()),
10358            ..Default::default()
10359        }
10360    }
10361
10362    fn fake_noise_floor_histograms() -> Vec<fidl_fuchsia_wlan_stats::NoiseFloorHistogram> {
10363        vec![fidl_fuchsia_wlan_stats::NoiseFloorHistogram {
10364            hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
10365            antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
10366                freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G,
10367                index: 0,
10368            })),
10369            noise_floor_samples: vec![
10370                // We normally don't expect the driver to send buckets with zero samples, but
10371                // mock them here anyway so we can test that we filter them out if they exist.
10372                fidl_fuchsia_wlan_stats::HistBucket { bucket_index: 199, num_samples: 0 },
10373                fidl_fuchsia_wlan_stats::HistBucket { bucket_index: 200, num_samples: 999 },
10374            ],
10375            invalid_samples: 44,
10376        }]
10377    }
10378
10379    fn fake_rssi_histograms() -> Vec<fidl_fuchsia_wlan_stats::RssiHistogram> {
10380        vec![fidl_fuchsia_wlan_stats::RssiHistogram {
10381            hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
10382            antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
10383                freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G,
10384                index: 0,
10385            })),
10386            rssi_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
10387                bucket_index: 230,
10388                num_samples: 999,
10389            }],
10390            invalid_samples: 55,
10391        }]
10392    }
10393
10394    fn fake_rx_rate_index_histograms() -> Vec<fidl_fuchsia_wlan_stats::RxRateIndexHistogram> {
10395        vec![
10396            fidl_fuchsia_wlan_stats::RxRateIndexHistogram {
10397                hist_scope: fidl_fuchsia_wlan_stats::HistScope::Station,
10398                antenna_id: None,
10399                rx_rate_index_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
10400                    bucket_index: 99,
10401                    num_samples: 1400,
10402                }],
10403                invalid_samples: 22,
10404            },
10405            fidl_fuchsia_wlan_stats::RxRateIndexHistogram {
10406                hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
10407                antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
10408                    freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna5G,
10409                    index: 1,
10410                })),
10411                rx_rate_index_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
10412                    bucket_index: 100,
10413                    num_samples: 1500,
10414                }],
10415                invalid_samples: 33,
10416            },
10417        ]
10418    }
10419
10420    fn fake_snr_histograms() -> Vec<fidl_fuchsia_wlan_stats::SnrHistogram> {
10421        vec![fidl_fuchsia_wlan_stats::SnrHistogram {
10422            hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
10423            antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
10424                freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G,
10425                index: 0,
10426            })),
10427            snr_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
10428                bucket_index: 30,
10429                num_samples: 999,
10430            }],
10431            invalid_samples: 11,
10432        }]
10433    }
10434
10435    fn fake_disconnect_info() -> DisconnectInfo {
10436        let is_sme_reconnecting = false;
10437        let fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting);
10438        DisconnectInfo {
10439            iface_id: IFACE_ID,
10440            connected_duration: zx::MonotonicDuration::from_hours(6),
10441            is_sme_reconnecting: fidl_disconnect_info.is_sme_reconnecting,
10442            disconnect_source: fidl_disconnect_info.disconnect_source,
10443            previous_connect_reason: client::types::ConnectReason::IdleInterfaceAutoconnect,
10444            ap_state: random_bss_description!(Wpa2).into(),
10445            signals: HistoricalList::new(),
10446        }
10447    }
10448
10449    fn fake_connect_result(code: fidl_ieee80211::StatusCode) -> fidl_sme::ConnectResult {
10450        fidl_sme::ConnectResult { code, is_credential_rejected: false, is_reconnect: false }
10451    }
10452
10453    #[fuchsia::test]
10454    fn test_error_throttling() {
10455        let exec = fasync::TestExecutor::new_with_fake_time();
10456        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
10457        let mut error_logger = ThrottledErrorLogger::new(MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS);
10458
10459        // Set the fake time to 61 minutes past 0 time to ensure that messages will be logged.
10460        exec.set_fake_time(fasync::MonotonicInstant::after(
10461            fasync::MonotonicDuration::from_minutes(MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS + 1),
10462        ));
10463
10464        // Log an error and verify that no record of it was retained (ie: the error was emitted
10465        // immediately).
10466        error_logger.throttle_error(Err(format_err!("")));
10467        assert!(!error_logger.suppressed_errors.contains_key(&String::from("")));
10468
10469        // Log another error and verify that the error counter has been incremented.
10470        error_logger.throttle_error(Err(format_err!("")));
10471        assert_eq!(error_logger.suppressed_errors[&String::from("")], 1);
10472
10473        // Advance time again and log another error to verify that the counter resets (ie: log was
10474        // emitted).
10475        exec.set_fake_time(fasync::MonotonicInstant::after(
10476            fasync::MonotonicDuration::from_minutes(MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS + 1),
10477        ));
10478        error_logger.throttle_error(Err(format_err!("")));
10479        assert!(!error_logger.suppressed_errors.contains_key(&String::from("")));
10480
10481        // Log another error to verify that the counter begins incrementing again.
10482        error_logger.throttle_error(Err(format_err!("")));
10483        assert_eq!(error_logger.suppressed_errors[&String::from("")], 1);
10484    }
10485
10486    #[fuchsia::test]
10487    fn test_log_chip_power_up_failure() {
10488        let (mut test_helper, mut test_fut) = setup_test();
10489
10490        test_helper.telemetry_sender.send(TelemetryEvent::ChipPowerUpFailure);
10491        test_helper.drain_cobalt_events(&mut test_fut);
10492
10493        let logged_metrics =
10494            test_helper.get_logged_metrics(metrics::CHIP_POWER_UP_FAILURE_METRIC_ID);
10495        assert_matches!(&logged_metrics[..], [metric] => {
10496            let expected_metric = fidl_fuchsia_metrics::MetricEvent {
10497                metric_id: metrics::CHIP_POWER_UP_FAILURE_METRIC_ID,
10498                event_codes: vec![],
10499                payload: fidl_fuchsia_metrics::MetricEventPayload::Count(1),
10500            };
10501            assert_eq!(metric, &expected_metric);
10502        });
10503    }
10504}