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