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