Skip to main content

wlancfg_lib/telemetry/
mod.rs

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