Skip to main content

wlancfg_lib/telemetry/
mod.rs

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