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