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 STEP_INCREMENT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(1);
4747    const IFACE_ID: u16 = 1;
4748
4749    // Macro rule for testing Inspect data tree. When we query for Inspect data, the LazyNode
4750    // will make a stats query req that we need to respond to in order to unblock the test.
4751    macro_rules! assert_data_tree_with_respond_blocking_req {
4752        ($test_helper:expr, $test_fut:expr, $($rest:tt)+) => {{
4753            use {
4754                fuchsia_inspect::reader, diagnostics_assertions::assert_data_tree,
4755            };
4756
4757            let inspector = $test_helper.inspector.clone();
4758            let read_fut = reader::read(&inspector);
4759            let mut read_fut = pin!(read_fut);
4760            loop {
4761                match $test_helper.exec.run_until_stalled(&mut read_fut) {
4762                    Poll::Pending => {
4763                        // Run telemetry test future so it can respond to QueryStatus request,
4764                        // while clearing out any potentially blocking Cobalt events
4765                        $test_helper.drain_cobalt_events(&mut $test_fut);
4766                        // Manually respond to iface stats request
4767                        if let Some(telemetry_svc_stream) = &mut $test_helper.telemetry_svc_stream {
4768                            if !telemetry_svc_stream.is_terminated() {
4769                                respond_iface_histogram_stats_req(
4770                                    &mut $test_helper.exec,
4771                                    telemetry_svc_stream,
4772                                );
4773                            }
4774                        }
4775
4776                    }
4777                    Poll::Ready(result) => {
4778                        let hierarchy = result.expect("failed to get hierarchy");
4779                        assert_data_tree!(@executor $test_helper.exec, hierarchy, $($rest)+);
4780                        break
4781                    }
4782                }
4783            }
4784        }}
4785    }
4786
4787    #[fuchsia::test]
4788    fn test_detect_driver_unresponsive_signal_ind() {
4789        let (mut test_helper, mut test_fut) = setup_test();
4790        test_helper.send_connected_event(random_bss_description!(Wpa2));
4791
4792        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4793            stats: contains {
4794                is_driver_unresponsive: false,
4795            }
4796        });
4797
4798        test_helper.advance_by(
4799            UNRESPONSIVE_FLAG_MIN_DURATION - TELEMETRY_QUERY_INTERVAL,
4800            test_fut.as_mut(),
4801        );
4802        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4803            stats: contains {
4804                is_driver_unresponsive: false,
4805            }
4806        });
4807
4808        // Send a signal, which resets timing information for determining driver unresponsiveness
4809        let ind = fidl_internal::SignalReportIndication { rssi_dbm: -40, snr_db: 30 };
4810        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind });
4811
4812        test_helper.advance_by(UNRESPONSIVE_FLAG_MIN_DURATION, test_fut.as_mut());
4813        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4814            stats: contains {
4815                is_driver_unresponsive: false,
4816            }
4817        });
4818
4819        // On the next telemetry interval, driver is recognized as unresponsive
4820        test_helper.advance_by(TELEMETRY_QUERY_INTERVAL, test_fut.as_mut());
4821        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4822            stats: contains {
4823                is_driver_unresponsive: true,
4824            }
4825        });
4826    }
4827
4828    #[fuchsia::test]
4829    fn test_histogram_stats_timeout() {
4830        let mut exec = fasync::TestExecutor::new();
4831
4832        let inspector = Inspector::default();
4833        let external_node = inspector.root().create_child("external");
4834        let external_inspect_node = ExternalInspectNode::new(external_node);
4835
4836        let (telemetry_sender, mut telemetry_receiver) =
4837            mpsc::channel::<TelemetryEvent>(TELEMETRY_EVENT_BUFFER_SIZE);
4838        let (defect_sender, mut defect_receiver) = mpsc::channel(100);
4839
4840        // Setup the lazy child node.  When the inspect node is read, it will snapshot current
4841        // interface state.
4842        inspect_record_external_data(
4843            &external_inspect_node,
4844            TelemetrySender::new(telemetry_sender),
4845            defect_sender,
4846        );
4847
4848        // Initiate a read of the inspect node.  This will run the future that was constructed.
4849        let fut = reader::read(&inspector);
4850        let mut fut = pin!(fut);
4851        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4852
4853        // First, inspect will query the current state from the telemetry event loop.  In order to
4854        // get to the point of querying histograms, we need to reply that we are in the connected
4855        // state.
4856        let (telemetry_proxy, _telemetry_server) =
4857            fidl::endpoints::create_proxy::<fidl_sme::TelemetryMarker>();
4858        assert_matches!(
4859            telemetry_receiver.try_next(),
4860            Ok(Some(TelemetryEvent::QueryStatus {sender})) => {
4861                sender.send(QueryStatusResult {
4862                    connection_state: ConnectionStateInfo::Connected {
4863                        iface_id: 0,
4864                        ap_state: Box::new(random_bss_description!(Wpa2).into()),
4865                        telemetry_proxy: Some(telemetry_proxy)
4866                    }
4867                }).expect("failed to send query status result")
4868            }
4869        );
4870        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4871
4872        // The future should block on getting the histogram stats until the timer expires.
4873        assert!(exec.wake_next_timer().is_some());
4874        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(_));
4875
4876        // We should get a timeout defect.
4877        assert_matches!(
4878            defect_receiver.try_next(),
4879            Ok(Some(Defect::Iface(IfaceFailure::Timeout {
4880                iface_id: 0,
4881                source: TimeoutSource::GetHistogramStats,
4882            })))
4883        );
4884    }
4885
4886    #[fuchsia::test]
4887    fn test_telemetry_timeout() {
4888        let mut exec = fasync::TestExecutor::new();
4889
4890        // Boilerplate for creating a Telemetry struct
4891        let (sender, _receiver) = mpsc::channel::<TelemetryEvent>(TELEMETRY_EVENT_BUFFER_SIZE);
4892        let (monitor_svc_proxy, _monitor_svc_stream) =
4893            create_proxy_and_stream::<fidl_fuchsia_wlan_device_service::DeviceMonitorMarker>();
4894        let (cobalt_proxy, _cobalt_stream) =
4895            create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
4896        let inspector = Inspector::default();
4897        let inspect_node = inspector.root().create_child("stats");
4898        let external_inspect_node = inspector.root().create_child("external");
4899        let (defect_sender, mut defect_receiver) = mpsc::channel(100);
4900
4901        let mut telemetry = Telemetry::new(
4902            TelemetrySender::new(sender),
4903            monitor_svc_proxy,
4904            cobalt_proxy.clone(),
4905            inspect_node,
4906            external_inspect_node,
4907            defect_sender,
4908        );
4909
4910        // Setup the Telemetry struct so that it thinks that it is connected.
4911        let (telemetry_proxy, _telemetry_server) =
4912            fidl::endpoints::create_proxy::<fidl_sme::TelemetryMarker>();
4913        telemetry.connection_state = ConnectionState::Connected(Box::new(ConnectedState {
4914            iface_id: 0,
4915            ap_state: Box::new(random_bss_description!(Wpa2).into()),
4916            telemetry_proxy: Some(telemetry_proxy),
4917
4918            // The rest of the fields don't matter for this test case.
4919            new_connect_start_time: None,
4920            prev_connection_stats: None,
4921            multiple_bss_candidates: false,
4922            network_is_likely_hidden: false,
4923            last_signal_report: fasync::MonotonicInstant::now(),
4924            num_consecutive_get_counter_stats_failures: InspectableU64::new(
4925                0,
4926                &telemetry.inspect_node,
4927                "num_consecutive_get_counter_stats_failures",
4928            ),
4929            is_driver_unresponsive: InspectableBool::new(
4930                false,
4931                &telemetry.inspect_node,
4932                "is_driver_unresponsive",
4933            ),
4934        }));
4935
4936        // Call handle_periodic_telemetry.
4937        let fut = telemetry.handle_periodic_telemetry();
4938        let mut fut = pin!(fut);
4939        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4940
4941        // Have the executor trigger the timeout.
4942        assert!(exec.wake_next_timer().is_some());
4943        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4944
4945        // Verify that the timeout has been received.
4946        assert_matches!(
4947            defect_receiver.try_next(),
4948            Ok(Some(Defect::Iface(IfaceFailure::Timeout {
4949                iface_id: 0,
4950                source: TimeoutSource::GetIfaceStats,
4951            })))
4952        );
4953    }
4954
4955    #[fuchsia::test]
4956    fn test_logging_num_consecutive_get_iface_stats_failures() {
4957        let (mut test_helper, mut test_fut) = setup_test();
4958        test_helper.set_iface_stats_resp(Box::new(|| Err(zx::sys::ZX_ERR_TIMED_OUT)));
4959        test_helper.send_connected_event(random_bss_description!(Wpa2));
4960
4961        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4962            stats: contains {
4963                num_consecutive_get_counter_stats_failures: 0u64,
4964            }
4965        });
4966
4967        test_helper.advance_by(TELEMETRY_QUERY_INTERVAL * 20i64, test_fut.as_mut());
4968        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4969            stats: contains {
4970                num_consecutive_get_counter_stats_failures: 20u64,
4971            }
4972        });
4973
4974        // Expect that Cobalt has been notified.
4975        test_helper.drain_cobalt_events(&mut test_fut);
4976        let logged_metrics =
4977            test_helper.get_logged_metrics(metrics::CONSECUTIVE_COUNTER_STATS_FAILURES_METRIC_ID);
4978        assert_eq!(logged_metrics.len(), 20);
4979
4980        assert_eq!(
4981            logged_metrics[19].payload,
4982            fidl_fuchsia_metrics::MetricEventPayload::IntegerValue(20)
4983        );
4984    }
4985
4986    #[fuchsia::test]
4987    fn test_log_connect_event_correct_shape() {
4988        let (mut test_helper, mut test_fut) = setup_test();
4989        test_helper.send_connected_event(random_bss_description!(Wpa2));
4990
4991        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
4992
4993        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4994            stats: contains {
4995                connect_events: {
4996                    "0": {
4997                        "@time": AnyNumericProperty,
4998                        multiple_bss_candidates: AnyBoolProperty,
4999                        network: {
5000                            bssid: &*BSSID_REGEX,
5001                            ssid: &*SSID_REGEX,
5002                            rssi_dbm: AnyNumericProperty,
5003                            snr_db: AnyNumericProperty,
5004                        }
5005                    }
5006                }
5007            }
5008        });
5009    }
5010
5011    #[fuchsia::test]
5012    fn test_log_connection_status_correct_shape() {
5013        let (mut test_helper, mut test_fut) = setup_test();
5014        test_helper.send_connected_event(random_bss_description!(Wpa2));
5015
5016        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5017
5018        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5019            stats: contains {
5020                connection_status: contains {
5021                    status_string: AnyStringProperty,
5022                    connected_network: contains {
5023                        rssi_dbm: AnyNumericProperty,
5024                        snr_db: AnyNumericProperty,
5025                        bssid: &*BSSID_REGEX,
5026                        ssid: &*SSID_REGEX,
5027                        protection: AnyStringProperty,
5028                        channel: AnyStringProperty,
5029                        is_wmm_assoc: AnyBoolProperty,
5030                    }
5031                }
5032            }
5033        });
5034    }
5035
5036    #[allow(clippy::regex_creation_in_loops, reason = "mass allow for https://fxbug.dev/381896734")]
5037    #[fuchsia::test]
5038    fn test_log_disconnect_event_correct_shape() {
5039        let (mut test_helper, mut test_fut) = setup_test();
5040
5041        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5042            track_subsequent_downtime: false,
5043            info: Some(fake_disconnect_info()),
5044        });
5045        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5046
5047        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5048            external: contains {
5049                stats: contains {
5050                    disconnect_events: {
5051                        "0": {
5052                            "@time": AnyNumericProperty,
5053                            flattened_reason_code: AnyNumericProperty,
5054                            locally_initiated: AnyBoolProperty,
5055                            network: {
5056                                channel: {
5057                                    primary: AnyNumericProperty,
5058                                }
5059                            }
5060                        }
5061                    }
5062                }
5063            },
5064            stats: contains {
5065                disconnect_events: {
5066                    "0": {
5067                        "@time": AnyNumericProperty,
5068                        connected_duration: AnyNumericProperty,
5069                        disconnect_source: Regex::new("^source: [^,]+, reason: [^,]+(?:, mlme_event_name: [^,]+)?$").unwrap(),
5070                        network: contains {
5071                            rssi_dbm: AnyNumericProperty,
5072                            snr_db: AnyNumericProperty,
5073                            bssid: &*BSSID_REGEX,
5074                            ssid: &*SSID_REGEX,
5075                            protection: AnyStringProperty,
5076                            channel: AnyStringProperty,
5077                            is_wmm_assoc: AnyBoolProperty,
5078                        }
5079                    }
5080                }
5081            }
5082        });
5083    }
5084
5085    #[fuchsia::test]
5086    fn test_log_disconnect_on_recovery() {
5087        let mut exec = fasync::TestExecutor::new();
5088
5089        // Boilerplate for creating a Telemetry struct
5090        let (sender, _receiver) = mpsc::channel::<TelemetryEvent>(TELEMETRY_EVENT_BUFFER_SIZE);
5091        let (monitor_svc_proxy, _monitor_svc_stream) =
5092            create_proxy_and_stream::<fidl_fuchsia_wlan_device_service::DeviceMonitorMarker>();
5093        let (cobalt_1dot1_proxy, mut cobalt_1dot1_stream) =
5094            create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
5095        let inspector = Inspector::default();
5096        let inspect_node = inspector.root().create_child("stats");
5097        let external_inspect_node = inspector.root().create_child("external");
5098        let (defect_sender, _defect_receiver) = mpsc::channel(100);
5099
5100        // Create a telemetry struct and initialize it to be in the connected state.
5101        let mut telemetry = Telemetry::new(
5102            TelemetrySender::new(sender),
5103            monitor_svc_proxy,
5104            cobalt_1dot1_proxy.clone(),
5105            inspect_node,
5106            external_inspect_node,
5107            defect_sender,
5108        );
5109
5110        telemetry.connection_state = ConnectionState::Connected(Box::new(ConnectedState {
5111            iface_id: 0,
5112            new_connect_start_time: None,
5113            prev_connection_stats: None,
5114            multiple_bss_candidates: false,
5115            ap_state: Box::new(generate_random_ap_state()),
5116            network_is_likely_hidden: false,
5117            last_signal_report: fasync::MonotonicInstant::now(),
5118            num_consecutive_get_counter_stats_failures: InspectableU64::new(
5119                0,
5120                &telemetry.inspect_node,
5121                "num_consecutive_get_counter_stats_failures",
5122            ),
5123            is_driver_unresponsive: InspectableBool::new(
5124                false,
5125                &telemetry.inspect_node,
5126                "is_driver_unresponsive",
5127            ),
5128            telemetry_proxy: None,
5129        }));
5130
5131        {
5132            // Send a disconnect event with empty disconnect info.
5133            let fut = telemetry.handle_telemetry_event(TelemetryEvent::Disconnected {
5134                track_subsequent_downtime: false,
5135                info: None,
5136            });
5137            let mut fut = pin!(fut);
5138
5139            assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
5140
5141            // There should be a single batch logging event.
5142            assert_matches!(
5143                exec.run_until_stalled(&mut cobalt_1dot1_stream.next()),
5144                Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerRequest::LogMetricEvents {
5145                    events: _,
5146                    responder
5147                }))) => {
5148                    responder.send(Ok(())).expect("failed to send response");
5149                }
5150            );
5151
5152            // And then the future should run to completion.
5153            assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
5154        }
5155
5156        // Verify that the telemetry state has transitioned to idle.
5157        assert_matches!(
5158            telemetry.connection_state,
5159            ConnectionState::Idle(IdleState { connect_start_time: None })
5160        );
5161    }
5162
5163    #[fuchsia::test]
5164    fn test_stat_cycles() {
5165        let (mut test_helper, mut test_fut) = setup_test();
5166        test_helper.send_connected_event(random_bss_description!(Wpa2));
5167        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5168
5169        test_helper.advance_by(
5170            zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL,
5171            test_fut.as_mut(),
5172        );
5173        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5174            stats: contains {
5175                "1d_counters": contains {
5176                    total_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5177                    connected_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5178                },
5179                "7d_counters": contains {
5180                    total_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5181                    connected_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5182                },
5183            }
5184        });
5185
5186        test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5187        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5188            stats: contains {
5189                "1d_counters": contains {
5190                    // The first hour window is now discarded, so it only shows 23 hours
5191                    // of total and connected duration.
5192                    total_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
5193                    connected_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
5194                },
5195                "7d_counters": contains {
5196                    total_duration: zx::MonotonicDuration::from_hours(24).into_nanos(),
5197                    connected_duration: zx::MonotonicDuration::from_hours(24).into_nanos(),
5198                },
5199            }
5200        });
5201
5202        test_helper.advance_by(zx::MonotonicDuration::from_hours(2), test_fut.as_mut());
5203        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5204            stats: contains {
5205                "1d_counters": contains {
5206                    total_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
5207                    connected_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
5208                },
5209                "7d_counters": contains {
5210                    total_duration: zx::MonotonicDuration::from_hours(26).into_nanos(),
5211                    connected_duration: zx::MonotonicDuration::from_hours(26).into_nanos(),
5212                },
5213            }
5214        });
5215
5216        // Disconnect now
5217        let info = fake_disconnect_info();
5218        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5219            track_subsequent_downtime: false,
5220            info: Some(info),
5221        });
5222        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5223
5224        test_helper.advance_by(zx::MonotonicDuration::from_hours(8), test_fut.as_mut());
5225        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5226            stats: contains {
5227                "1d_counters": contains {
5228                    total_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
5229                    // Now the 1d connected counter should decrease
5230                    connected_duration: zx::MonotonicDuration::from_hours(15).into_nanos(),
5231                },
5232                "7d_counters": contains {
5233                    total_duration: zx::MonotonicDuration::from_hours(34).into_nanos(),
5234                    connected_duration: zx::MonotonicDuration::from_hours(26).into_nanos(),
5235                },
5236            }
5237        });
5238
5239        // The 7d counters do not decrease before the 7th day
5240        test_helper.advance_by(zx::MonotonicDuration::from_hours(14), test_fut.as_mut());
5241        test_helper.advance_by(
5242            zx::MonotonicDuration::from_hours(5 * 24) - TELEMETRY_QUERY_INTERVAL,
5243            test_fut.as_mut(),
5244        );
5245        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5246            stats: contains {
5247                "1d_counters": contains {
5248                    total_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5249                    connected_duration: 0i64,
5250                },
5251                "7d_counters": contains {
5252                    total_duration: (zx::MonotonicDuration::from_hours(7 * 24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5253                    connected_duration: zx::MonotonicDuration::from_hours(26).into_nanos(),
5254                },
5255            }
5256        });
5257
5258        // On the 7th day, the first window is removed (24 hours of duration is deducted)
5259        test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5260        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5261            stats: contains {
5262                "1d_counters": contains {
5263                    total_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
5264                    connected_duration: 0i64,
5265                },
5266                "7d_counters": contains {
5267                    total_duration: zx::MonotonicDuration::from_hours(6 * 24).into_nanos(),
5268                    connected_duration: zx::MonotonicDuration::from_hours(2).into_nanos(),
5269                },
5270            }
5271        });
5272    }
5273
5274    #[fuchsia::test]
5275    fn test_daily_detailed_stat_cycles() {
5276        let (mut test_helper, mut test_fut) = setup_test();
5277        for _ in 0..10 {
5278            test_helper.send_connected_event(random_bss_description!(Wpa2));
5279        }
5280        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
5281
5282        // On 1st day, 10 successful connects, so verify metric is logged with count of 10.
5283        let status_codes = test_helper.get_logged_metrics(
5284            metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
5285        );
5286        assert_eq!(status_codes.len(), 1);
5287        assert_eq!(
5288            status_codes[0].event_codes,
5289            vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
5290        );
5291        assert_eq!(status_codes[0].payload, MetricEventPayload::Count(10));
5292
5293        test_helper.cobalt_events.clear();
5294
5295        test_helper.send_connected_event(random_bss_description!(Wpa2));
5296        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
5297
5298        // On 2nd day, 1 successful connect, so verify metric is logged with count of 1.
5299        let status_codes = test_helper.get_logged_metrics(
5300            metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
5301        );
5302        assert_eq!(status_codes.len(), 1);
5303        assert_eq!(
5304            status_codes[0].event_codes,
5305            vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
5306        );
5307        assert_eq!(status_codes[0].payload, MetricEventPayload::Count(1));
5308    }
5309
5310    #[fuchsia::test]
5311    fn test_total_duration_counters() {
5312        let (mut test_helper, mut test_fut) = setup_test();
5313
5314        test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5315        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5316            stats: contains {
5317                "1d_counters": contains {
5318                    total_duration: zx::MonotonicDuration::from_minutes(30).into_nanos(),
5319                },
5320                "7d_counters": contains {
5321                    total_duration: zx::MonotonicDuration::from_minutes(30).into_nanos(),
5322                },
5323            }
5324        });
5325
5326        test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5327        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5328            stats: contains {
5329                "1d_counters": contains {
5330                    total_duration: zx::MonotonicDuration::from_hours(1).into_nanos(),
5331                },
5332                "7d_counters": contains {
5333                    total_duration: zx::MonotonicDuration::from_hours(1).into_nanos(),
5334                },
5335            }
5336        });
5337    }
5338
5339    #[fuchsia::test]
5340    fn test_total_duration_time_series() {
5341        let (mut test_helper, mut test_fut) = setup_test();
5342
5343        test_helper.advance_by(zx::MonotonicDuration::from_seconds(25), test_fut.as_mut());
5344        let time_series = test_helper.get_time_series(&mut test_fut);
5345        let total_duration_sec: Vec<_> =
5346            time_series.lock().total_duration_sec.minutely_iter().copied().collect();
5347        assert_eq!(total_duration_sec, vec![15]);
5348
5349        test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5350        let total_duration_sec: Vec<_> =
5351            time_series.lock().total_duration_sec.minutely_iter().copied().collect();
5352        assert_eq!(total_duration_sec, vec![30]);
5353    }
5354
5355    #[fuchsia::test]
5356    fn test_counters_when_idle() {
5357        let (mut test_helper, mut test_fut) = setup_test();
5358
5359        test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5360        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5361            stats: contains {
5362                "1d_counters": contains {
5363                    connected_duration: 0i64,
5364                    downtime_duration: 0i64,
5365                    downtime_no_saved_neighbor_duration: 0i64,
5366                },
5367                "7d_counters": contains {
5368                    connected_duration: 0i64,
5369                    downtime_duration: 0i64,
5370                    downtime_no_saved_neighbor_duration: 0i64,
5371                },
5372            }
5373        });
5374
5375        test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5376        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5377            stats: contains {
5378                "1d_counters": contains {
5379                    connected_duration: 0i64,
5380                    downtime_duration: 0i64,
5381                    downtime_no_saved_neighbor_duration: 0i64,
5382                },
5383                "7d_counters": contains {
5384                    connected_duration: 0i64,
5385                    downtime_duration: 0i64,
5386                    downtime_no_saved_neighbor_duration: 0i64,
5387                },
5388            }
5389        });
5390    }
5391
5392    #[fuchsia::test]
5393    fn test_connected_counters_increase_when_connected() {
5394        let (mut test_helper, mut test_fut) = setup_test();
5395        test_helper.send_connected_event(random_bss_description!(Wpa2));
5396        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5397
5398        test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5399        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5400            stats: contains {
5401                "1d_counters": contains {
5402                    connected_duration: zx::MonotonicDuration::from_minutes(30).into_nanos(),
5403                    downtime_duration: 0i64,
5404                    downtime_no_saved_neighbor_duration: 0i64,
5405                },
5406                "7d_counters": contains {
5407                    connected_duration: zx::MonotonicDuration::from_minutes(30).into_nanos(),
5408                    downtime_duration: 0i64,
5409                    downtime_no_saved_neighbor_duration: 0i64,
5410                },
5411            }
5412        });
5413
5414        test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5415        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5416            stats: contains {
5417                "1d_counters": contains {
5418                    connected_duration: zx::MonotonicDuration::from_hours(1).into_nanos(),
5419                    downtime_duration: 0i64,
5420                    downtime_no_saved_neighbor_duration: 0i64,
5421                },
5422                "7d_counters": contains {
5423                    connected_duration: zx::MonotonicDuration::from_hours(1).into_nanos(),
5424                    downtime_duration: 0i64,
5425                    downtime_no_saved_neighbor_duration: 0i64,
5426                },
5427            }
5428        });
5429    }
5430
5431    #[fuchsia::test]
5432    fn test_downtime_counter() {
5433        let (mut test_helper, mut test_fut) = setup_test();
5434
5435        // Disconnect but not track downtime. Downtime counter should not increase.
5436        let info = fake_disconnect_info();
5437        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5438            track_subsequent_downtime: false,
5439            info: Some(info),
5440        });
5441        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5442
5443        test_helper.advance_by(zx::MonotonicDuration::from_minutes(10), test_fut.as_mut());
5444
5445        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5446            stats: contains {
5447                "1d_counters": contains {
5448                    connected_duration: 0i64,
5449                    downtime_duration: 0i64,
5450                    downtime_no_saved_neighbor_duration: 0i64,
5451                },
5452                "7d_counters": contains {
5453                    connected_duration: 0i64,
5454                    downtime_duration: 0i64,
5455                    downtime_no_saved_neighbor_duration: 0i64,
5456                },
5457            }
5458        });
5459
5460        // Disconnect and track downtime. Downtime counter should now increase
5461        let info = fake_disconnect_info();
5462        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5463            track_subsequent_downtime: true,
5464            info: Some(info),
5465        });
5466        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5467
5468        test_helper.advance_by(zx::MonotonicDuration::from_minutes(15), test_fut.as_mut());
5469
5470        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5471            stats: contains {
5472                "1d_counters": contains {
5473                    connected_duration: 0i64,
5474                    downtime_duration: zx::MonotonicDuration::from_minutes(15).into_nanos(),
5475                    downtime_no_saved_neighbor_duration: 0i64,
5476                },
5477                "7d_counters": contains {
5478                    connected_duration: 0i64,
5479                    downtime_duration: zx::MonotonicDuration::from_minutes(15).into_nanos(),
5480                    downtime_no_saved_neighbor_duration: 0i64,
5481                },
5482            }
5483        });
5484    }
5485
5486    #[fuchsia::test]
5487    fn test_counters_connect_then_disconnect() {
5488        let (mut test_helper, mut test_fut) = setup_test();
5489        test_helper.send_connected_event(random_bss_description!(Wpa2));
5490        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5491
5492        test_helper.advance_by(zx::MonotonicDuration::from_seconds(5), test_fut.as_mut());
5493
5494        // Disconnect but not track downtime. Downtime counter should not increase.
5495        let info = fake_disconnect_info();
5496        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5497            track_subsequent_downtime: true,
5498            info: Some(info),
5499        });
5500        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5501
5502        // The 5 seconds connected duration is not accounted for yet.
5503        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5504            stats: contains {
5505                "1d_counters": contains {
5506                    connected_duration: 0i64,
5507                    downtime_duration: 0i64,
5508                    downtime_no_saved_neighbor_duration: 0i64,
5509                },
5510                "7d_counters": contains {
5511                    connected_duration: 0i64,
5512                    downtime_duration: 0i64,
5513                    downtime_no_saved_neighbor_duration: 0i64,
5514                },
5515            }
5516        });
5517
5518        // At next telemetry checkpoint, `test_fut` updates the connected and downtime durations.
5519        let downtime_start = fasync::MonotonicInstant::now();
5520        test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5521        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5522            stats: contains {
5523                "1d_counters": contains {
5524                    connected_duration: zx::MonotonicDuration::from_seconds(5).into_nanos(),
5525                    downtime_duration: (fasync::MonotonicInstant::now() - downtime_start).into_nanos(),
5526                    downtime_no_saved_neighbor_duration: 0i64,
5527                },
5528                "7d_counters": contains {
5529                    connected_duration: zx::MonotonicDuration::from_seconds(5).into_nanos(),
5530                    downtime_duration: (fasync::MonotonicInstant::now() - downtime_start).into_nanos(),
5531                    downtime_no_saved_neighbor_duration: 0i64,
5532                },
5533            }
5534        });
5535    }
5536
5537    #[fuchsia::test]
5538    fn test_downtime_no_saved_neighbor_duration_counter() {
5539        let (mut test_helper, mut test_fut) = setup_test();
5540        test_helper.send_connected_event(random_bss_description!(Wpa2));
5541        test_helper.drain_cobalt_events(&mut test_fut);
5542
5543        // Disconnect and track downtime.
5544        let info = fake_disconnect_info();
5545        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5546            track_subsequent_downtime: true,
5547            info: Some(info),
5548        });
5549        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5550
5551        test_helper.advance_by(zx::MonotonicDuration::from_seconds(5), test_fut.as_mut());
5552        // Indicate that there's no saved neighbor in vicinity
5553        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
5554            network_selection_type: NetworkSelectionType::Undirected,
5555            num_candidates: Ok(0),
5556            selected_count: 0,
5557        });
5558        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5559
5560        test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5561        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5562            stats: contains {
5563                "1d_counters": contains {
5564                    connected_duration: 0i64,
5565                    downtime_duration: TELEMETRY_QUERY_INTERVAL.into_nanos(),
5566                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5567                },
5568                "7d_counters": contains {
5569                    connected_duration: 0i64,
5570                    downtime_duration: TELEMETRY_QUERY_INTERVAL.into_nanos(),
5571                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5572                },
5573            }
5574        });
5575
5576        test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5577        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5578            stats: contains {
5579                "1d_counters": contains {
5580                    connected_duration: 0i64,
5581                    downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5582                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5583                },
5584                "7d_counters": contains {
5585                    connected_duration: 0i64,
5586                    downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5587                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5588                },
5589            }
5590        });
5591
5592        test_helper.advance_by(zx::MonotonicDuration::from_seconds(5), test_fut.as_mut());
5593        // Indicate that saved neighbor has been found
5594        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
5595            network_selection_type: NetworkSelectionType::Undirected,
5596            num_candidates: Ok(1),
5597            selected_count: 0,
5598        });
5599        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5600
5601        // `downtime_no_saved_neighbor_duration` counter is not updated right away.
5602        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5603            stats: contains {
5604                "1d_counters": contains {
5605                    connected_duration: 0i64,
5606                    downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5607                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5608                },
5609                "7d_counters": contains {
5610                    connected_duration: 0i64,
5611                    downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5612                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5613                },
5614            }
5615        });
5616
5617        // At the next checkpoint, both downtime counters are updated together.
5618        test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5619        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5620            stats: contains {
5621                "1d_counters": contains {
5622                    connected_duration: 0i64,
5623                    downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
5624                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5625                },
5626                "7d_counters": contains {
5627                    connected_duration: 0i64,
5628                    downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
5629                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5630                },
5631            }
5632        });
5633
5634        // Disconnect but don't track downtime
5635        let info = fake_disconnect_info();
5636        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5637            track_subsequent_downtime: false,
5638            info: Some(info),
5639        });
5640
5641        // Indicate that there's no saved neighbor in vicinity
5642        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
5643            network_selection_type: NetworkSelectionType::Undirected,
5644            num_candidates: Ok(0),
5645            selected_count: 0,
5646        });
5647        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5648        test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5649
5650        // However, this time neither of the downtime counters should be incremented
5651        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5652            stats: contains {
5653                "1d_counters": contains {
5654                    connected_duration: 0i64,
5655                    downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
5656                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5657                },
5658                "7d_counters": contains {
5659                    connected_duration: 0i64,
5660                    downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
5661                    downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5662                },
5663            }
5664        });
5665    }
5666
5667    #[fuchsia::test]
5668    fn test_log_connect_attempt_counters() {
5669        let (mut test_helper, mut test_fut) = setup_test();
5670
5671        // Send 10 failed connect results, then 1 successful.
5672        for i in 0..10 {
5673            let event = TelemetryEvent::ConnectResult {
5674                iface_id: IFACE_ID,
5675                policy_connect_reason: Some(
5676                    client::types::ConnectReason::RetryAfterFailedConnectAttempt,
5677                ),
5678                result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
5679                multiple_bss_candidates: true,
5680                ap_state: random_bss_description!(Wpa1).into(),
5681                network_is_likely_hidden: false,
5682            };
5683            test_helper.telemetry_sender.send(event);
5684
5685            // Verify that the connection failure has been logged.
5686            test_helper.drain_cobalt_events(&mut test_fut);
5687            let logged_metrics =
5688                test_helper.get_logged_metrics(metrics::CONNECTION_FAILURES_METRIC_ID);
5689            assert_eq!(logged_metrics.len(), i + 1);
5690        }
5691        test_helper.send_connected_event(random_bss_description!(Wpa2));
5692        test_helper.drain_cobalt_events(&mut test_fut);
5693
5694        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5695            stats: contains {
5696                "1d_counters": contains {
5697                    connect_attempts_count: 11u64,
5698                    connect_successful_count: 1u64,
5699                },
5700                "7d_counters": contains {
5701                    connect_attempts_count: 11u64,
5702                    connect_successful_count: 1u64,
5703                },
5704            }
5705        });
5706    }
5707
5708    #[fuchsia::test]
5709    fn test_log_connect_attempt_time_series() {
5710        let (mut test_helper, mut test_fut) = setup_test();
5711
5712        // Send 10 failed connect results, then 1 successful.
5713        for i in 0..10 {
5714            let event = TelemetryEvent::ConnectResult {
5715                iface_id: IFACE_ID,
5716                policy_connect_reason: Some(
5717                    client::types::ConnectReason::RetryAfterFailedConnectAttempt,
5718                ),
5719                result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
5720                multiple_bss_candidates: true,
5721                ap_state: random_bss_description!(Wpa1).into(),
5722                network_is_likely_hidden: false,
5723            };
5724            test_helper.telemetry_sender.send(event);
5725
5726            // Verify that the connection failure has been logged.
5727            test_helper.drain_cobalt_events(&mut test_fut);
5728            let logged_metrics =
5729                test_helper.get_logged_metrics(metrics::CONNECTION_FAILURES_METRIC_ID);
5730            assert_eq!(logged_metrics.len(), i + 1);
5731        }
5732        test_helper.send_connected_event(random_bss_description!(Wpa2));
5733        test_helper.drain_cobalt_events(&mut test_fut);
5734        let time_series = test_helper.get_time_series(&mut test_fut);
5735        let connect_attempt_count: Vec<_> =
5736            time_series.lock().connect_attempt_count.minutely_iter().copied().collect();
5737        let connect_successful_count: Vec<_> =
5738            time_series.lock().connect_successful_count.minutely_iter().copied().collect();
5739        assert_eq!(connect_attempt_count, vec![11]);
5740        assert_eq!(connect_successful_count, vec![1]);
5741    }
5742
5743    #[fuchsia::test]
5744    fn test_disconnect_count_counter() {
5745        let (mut test_helper, mut test_fut) = setup_test();
5746        test_helper.send_connected_event(random_bss_description!(Wpa2));
5747        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5748
5749        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5750            stats: contains {
5751                "1d_counters": contains {
5752                    disconnect_count: 0u64,
5753                    policy_roam_disconnects_count: 0u64,
5754                },
5755                "7d_counters": contains {
5756                    disconnect_count: 0u64,
5757                    policy_roam_disconnects_count: 0u64,
5758                },
5759            }
5760        });
5761
5762        let info = DisconnectInfo {
5763            disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
5764                reason_code: fidl_ieee80211::ReasonCode::StaLeaving,
5765                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DisassociateIndication,
5766            }),
5767            ..fake_disconnect_info()
5768        };
5769        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5770            track_subsequent_downtime: true,
5771            info: Some(info),
5772        });
5773        test_helper.drain_cobalt_events(&mut test_fut);
5774
5775        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5776            stats: contains {
5777                "1d_counters": contains {
5778                    disconnect_count: 1u64,
5779                    policy_roam_disconnects_count: 0u64,
5780                    total_non_roam_disconnect_count: 1u64,
5781                    total_roam_disconnect_count: 0u64,
5782                },
5783                "7d_counters": contains {
5784                    disconnect_count: 1u64,
5785                    policy_roam_disconnects_count: 0u64,
5786                    total_non_roam_disconnect_count: 1u64,
5787                    total_roam_disconnect_count: 0u64,
5788                },
5789            }
5790        });
5791
5792        let info = DisconnectInfo {
5793            disconnect_source: fidl_sme::DisconnectSource::User(
5794                fidl_sme::UserDisconnectReason::Startup,
5795            ),
5796            ..fake_disconnect_info()
5797        };
5798        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5799            track_subsequent_downtime: false,
5800            info: Some(info),
5801        });
5802        test_helper.drain_cobalt_events(&mut test_fut);
5803
5804        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5805            stats: contains {
5806                "1d_counters": contains {
5807                    disconnect_count: 2u64,
5808                    policy_roam_disconnects_count: 0u64,
5809                    total_non_roam_disconnect_count: 2u64,
5810                    total_roam_disconnect_count: 0u64,
5811                },
5812                "7d_counters": contains {
5813                    disconnect_count: 2u64,
5814                    policy_roam_disconnects_count: 0u64,
5815                    total_non_roam_disconnect_count: 2u64,
5816                    total_roam_disconnect_count: 0u64,
5817                },
5818            }
5819        });
5820
5821        // Send a firmware initiated roam disconnect.
5822        let info = DisconnectInfo {
5823            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
5824                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
5825                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
5826            }),
5827            ..fake_disconnect_info()
5828        };
5829        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5830            track_subsequent_downtime: false,
5831            info: Some(info),
5832        });
5833        test_helper.drain_cobalt_events(&mut test_fut);
5834
5835        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5836            stats: contains {
5837                "1d_counters": contains {
5838                    disconnect_count: 3u64,
5839                    policy_roam_disconnects_count: 0u64,
5840                    total_non_roam_disconnect_count: 2u64,
5841                    total_roam_disconnect_count: 1u64,
5842                },
5843                "7d_counters": contains {
5844                    disconnect_count: 3u64,
5845                    policy_roam_disconnects_count: 0u64,
5846                    total_non_roam_disconnect_count: 2u64,
5847                    total_roam_disconnect_count: 1u64,
5848                },
5849            }
5850        });
5851    }
5852
5853    #[fuchsia::test]
5854    fn test_disconnect_count_time_series() {
5855        let (mut test_helper, mut test_fut) = setup_test();
5856        test_helper.send_connected_event(random_bss_description!(Wpa2));
5857        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5858
5859        let time_series = test_helper.get_time_series(&mut test_fut);
5860        let disconnect_count: Vec<_> =
5861            time_series.lock().disconnect_count.minutely_iter().copied().collect();
5862        assert_eq!(disconnect_count, vec![0]);
5863
5864        let info = DisconnectInfo {
5865            disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
5866                reason_code: fidl_ieee80211::ReasonCode::StaLeaving,
5867                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DisassociateIndication,
5868            }),
5869            ..fake_disconnect_info()
5870        };
5871        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5872            track_subsequent_downtime: true,
5873            info: Some(info),
5874        });
5875        test_helper.drain_cobalt_events(&mut test_fut);
5876
5877        let time_series = test_helper.get_time_series(&mut test_fut);
5878        let disconnect_count: Vec<_> =
5879            time_series.lock().disconnect_count.minutely_iter().copied().collect();
5880        assert_eq!(disconnect_count, vec![1]);
5881    }
5882
5883    #[fuchsia::test]
5884    fn test_policy_roam_disconnects_count_counter() {
5885        let (mut test_helper, mut test_fut) = setup_test();
5886        test_helper.send_connected_event(random_bss_description!(Wpa2));
5887        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5888        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5889            stats: contains {
5890                "1d_counters": contains {
5891                    disconnect_count: 0u64,
5892                    policy_roam_disconnects_count: 0u64,
5893                    total_roam_disconnect_count: 0u64,
5894                    total_non_roam_disconnect_count: 0u64,
5895                },
5896                "7d_counters": contains {
5897                    disconnect_count: 0u64,
5898                    policy_roam_disconnects_count: 0u64,
5899                    total_roam_disconnect_count: 0u64,
5900                    total_non_roam_disconnect_count: 0u64,
5901                },
5902            }
5903        });
5904
5905        // Send a successful policy initiated roam result event.
5906        let mut roam_result = fidl_sme::RoamResult {
5907            bssid: [1, 1, 1, 1, 1, 1],
5908            status_code: fidl_ieee80211::StatusCode::Success,
5909            original_association_maintained: false,
5910            bss_description: Some(Box::new(random_fidl_bss_description!())),
5911            disconnect_info: None,
5912            is_credential_rejected: false,
5913        };
5914        test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
5915            iface_id: 1,
5916            result: roam_result.clone(),
5917            updated_ap_state: generate_random_ap_state(),
5918            original_ap_state: Box::new(generate_random_ap_state()),
5919            request: Box::new(generate_policy_roam_request([1, 1, 1, 1, 1, 1].into())),
5920            request_time: fasync::MonotonicInstant::now(),
5921            result_time: fasync::MonotonicInstant::now(),
5922        });
5923        test_helper.drain_cobalt_events(&mut test_fut);
5924        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5925            stats: contains {
5926                "1d_counters": contains {
5927                    disconnect_count: 0u64,
5928                    policy_roam_disconnects_count: 1u64,
5929                    // Total roam disconnects should still be zero, as those are logged in the
5930                    // disconnect metric event, not the PolicyInitiatedRoamResult event.
5931                    total_roam_disconnect_count: 0u64,
5932                    total_non_roam_disconnect_count: 0u64,
5933                },
5934                "7d_counters": contains {
5935                    disconnect_count: 0u64,
5936                    policy_roam_disconnects_count: 1u64,
5937                    // Total roam disconnects should still be zero, as those are logged in the
5938                    // disconnect metric event, not the PolicyInitiatedRoamResult event.
5939                    total_roam_disconnect_count: 0u64,
5940                    total_non_roam_disconnect_count: 0u64,
5941                },
5942            }
5943        });
5944
5945        // Send a failed policy initiated roam result event.
5946        roam_result.status_code = fidl_ieee80211::StatusCode::RefusedReasonUnspecified;
5947        roam_result.disconnect_info = Some(Box::new(generate_disconnect_info(false)));
5948        test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
5949            iface_id: 1,
5950            result: roam_result.clone(),
5951            updated_ap_state: generate_random_ap_state(),
5952            original_ap_state: Box::new(generate_random_ap_state()),
5953            request: Box::new(generate_policy_roam_request([1, 1, 1, 1, 1, 1].into())),
5954            request_time: fasync::MonotonicInstant::now(),
5955            result_time: fasync::MonotonicInstant::now(),
5956        });
5957        test_helper.drain_cobalt_events(&mut test_fut);
5958        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5959            stats: contains {
5960                "1d_counters": contains {
5961                    disconnect_count: 0u64,
5962                    policy_roam_disconnects_count: 2u64,
5963                    // Total roam disconnects should still be zero, as those are logged in the
5964                    // disconnect metric event, not the PolicyInitiatedRoamResult event.
5965                    total_roam_disconnect_count: 0u64,
5966                    total_non_roam_disconnect_count: 0u64,
5967                },
5968                "7d_counters": contains {
5969                    disconnect_count: 0u64,
5970                    policy_roam_disconnects_count: 2u64,
5971                    // Total roam disconnects should still be zero, as those are logged in the
5972                    // disconnect metric event, not the PolicyInitiatedRoamResult event.
5973                    total_roam_disconnect_count: 0u64,
5974                    total_non_roam_disconnect_count: 0u64,
5975                },
5976            }
5977        });
5978
5979        // Send a failed policy initiated roam result with association maintained.
5980        roam_result.original_association_maintained = true;
5981        test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
5982            iface_id: 1,
5983            result: roam_result,
5984            updated_ap_state: generate_random_ap_state(),
5985            original_ap_state: Box::new(generate_random_ap_state()),
5986            request: Box::new(generate_policy_roam_request([1, 1, 1, 1, 1, 1].into())),
5987            request_time: fasync::MonotonicInstant::now(),
5988            result_time: fasync::MonotonicInstant::now(),
5989        });
5990        test_helper.drain_cobalt_events(&mut test_fut);
5991        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5992            stats: contains {
5993                "1d_counters": contains {
5994                    disconnect_count: 0u64,
5995                    policy_roam_disconnects_count: 2u64,
5996                    total_roam_disconnect_count: 0u64,
5997                    total_non_roam_disconnect_count: 0u64,
5998                },
5999                "7d_counters": contains {
6000                    disconnect_count: 0u64,
6001                    policy_roam_disconnects_count: 2u64,
6002                    total_roam_disconnect_count: 0u64,
6003                    total_non_roam_disconnect_count: 0u64,
6004                },
6005            }
6006        });
6007    }
6008
6009    #[fuchsia::test]
6010    fn test_connected_duration_time_series() {
6011        let (mut test_helper, mut test_fut) = setup_test();
6012        test_helper.send_connected_event(random_bss_description!(Wpa2));
6013        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6014        test_helper.advance_by(zx::MonotonicDuration::from_seconds(90), test_fut.as_mut());
6015
6016        let time_series = test_helper.get_time_series(&mut test_fut);
6017        let connected_duration_sec: Vec<_> =
6018            time_series.lock().connected_duration_sec.minutely_iter().copied().collect();
6019        assert_eq!(connected_duration_sec, vec![45, 45]);
6020    }
6021
6022    #[fuchsia::test]
6023    fn test_rx_tx_counters_no_issue() {
6024        let (mut test_helper, mut test_fut) = setup_test();
6025        test_helper.send_connected_event(random_bss_description!(Wpa2));
6026        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6027
6028        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6029        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
6030            stats: contains {
6031                get_iface_stats_fail_count: 0u64,
6032                "1d_counters": contains {
6033                    tx_high_packet_drop_duration: 0i64,
6034                    rx_high_packet_drop_duration: 0i64,
6035                    tx_very_high_packet_drop_duration: 0i64,
6036                    rx_very_high_packet_drop_duration: 0i64,
6037                    no_rx_duration: 0i64,
6038                },
6039                "7d_counters": contains {
6040                    tx_high_packet_drop_duration: 0i64,
6041                    rx_high_packet_drop_duration: 0i64,
6042                    tx_very_high_packet_drop_duration: 0i64,
6043                    rx_very_high_packet_drop_duration: 0i64,
6044                    no_rx_duration: 0i64,
6045                },
6046            }
6047        });
6048    }
6049
6050    #[fuchsia::test]
6051    fn test_tx_high_packet_drop_duration_counters() {
6052        let (mut test_helper, mut test_fut) = setup_test();
6053        test_helper.set_iface_stats_resp(Box::new(|| {
6054            let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
6055            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6056                connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6057                    tx_total: Some(10 * seed),
6058                    tx_drop: Some(3 * seed),
6059                    ..fake_connection_stats(seed)
6060                }),
6061                ..Default::default()
6062            })
6063        }));
6064
6065        test_helper.send_connected_event(random_bss_description!(Wpa2));
6066        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6067
6068        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6069        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
6070            stats: contains {
6071                get_iface_stats_fail_count: 0u64,
6072                "1d_counters": contains {
6073                    // Deduct 15 seconds beecause there isn't packet counter to diff against in
6074                    // the first interval of telemetry
6075                    tx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6076                    rx_high_packet_drop_duration: 0i64,
6077                    tx_very_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6078                    rx_very_high_packet_drop_duration: 0i64,
6079                    no_rx_duration: 0i64,
6080                },
6081                "7d_counters": contains {
6082                    tx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6083                    rx_high_packet_drop_duration: 0i64,
6084                    tx_very_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6085                    rx_very_high_packet_drop_duration: 0i64,
6086                    no_rx_duration: 0i64,
6087                },
6088            }
6089        });
6090    }
6091
6092    #[fuchsia::test]
6093    fn test_rx_high_packet_drop_duration_counters() {
6094        let (mut test_helper, mut test_fut) = setup_test();
6095        test_helper.set_iface_stats_resp(Box::new(|| {
6096            let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
6097            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6098                connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6099                    rx_unicast_total: Some(10 * seed),
6100                    rx_unicast_drop: Some(3 * seed),
6101                    ..fake_connection_stats(seed)
6102                }),
6103                ..Default::default()
6104            })
6105        }));
6106
6107        test_helper.send_connected_event(random_bss_description!(Wpa2));
6108        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6109
6110        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6111        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
6112            stats: contains {
6113                get_iface_stats_fail_count: 0u64,
6114                "1d_counters": contains {
6115                    // Deduct 15 seconds beecause there isn't packet counter to diff against in
6116                    // the first interval of telemetry
6117                    rx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6118                    tx_high_packet_drop_duration: 0i64,
6119                    rx_very_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6120                    tx_very_high_packet_drop_duration: 0i64,
6121                    no_rx_duration: 0i64,
6122                },
6123                "7d_counters": contains {
6124                    rx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6125                    tx_high_packet_drop_duration: 0i64,
6126                    rx_very_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6127                    tx_very_high_packet_drop_duration: 0i64,
6128                    no_rx_duration: 0i64,
6129                },
6130            }
6131        });
6132    }
6133
6134    #[fuchsia::test]
6135    fn test_rx_tx_high_but_not_very_high_packet_drop_duration_counters() {
6136        let (mut test_helper, mut test_fut) = setup_test();
6137        test_helper.set_iface_stats_resp(Box::new(|| {
6138            let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
6139            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6140                connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6141                    // 3% drop rate would be high, but not very high
6142                    rx_unicast_total: Some(100 * seed),
6143                    rx_unicast_drop: Some(3 * seed),
6144                    tx_total: Some(100 * seed),
6145                    tx_drop: Some(3 * seed),
6146                    ..fake_connection_stats(seed)
6147                }),
6148                ..Default::default()
6149            })
6150        }));
6151
6152        test_helper.send_connected_event(random_bss_description!(Wpa2));
6153        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6154
6155        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6156        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
6157            stats: contains {
6158                get_iface_stats_fail_count: 0u64,
6159                "1d_counters": contains {
6160                    // Deduct 15 seconds beecause there isn't packet counter to diff against in
6161                    // the first interval of telemetry
6162                    rx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6163                    tx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6164                    // Very high drop rate counters should still be 0
6165                    rx_very_high_packet_drop_duration: 0i64,
6166                    tx_very_high_packet_drop_duration: 0i64,
6167                    no_rx_duration: 0i64,
6168                },
6169                "7d_counters": contains {
6170                    rx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6171                    tx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6172                    rx_very_high_packet_drop_duration: 0i64,
6173                    tx_very_high_packet_drop_duration: 0i64,
6174                    no_rx_duration: 0i64,
6175                },
6176            }
6177        });
6178    }
6179
6180    #[fuchsia::test]
6181    fn test_rx_tx_reset() {
6182        let (mut test_helper, mut test_fut) = setup_test();
6183        test_helper.set_iface_stats_resp(Box::new(|| {
6184            let seed = (fasync::MonotonicInstant::now() - fasync::MonotonicInstant::from_nanos(0))
6185                .into_seconds() as u64;
6186            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6187                connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6188                    rx_unicast_total: Some(999999 - seed),
6189                    rx_unicast_drop: Some(999999 - seed),
6190                    tx_total: Some(999999 - seed),
6191                    tx_drop: Some(999999 - seed),
6192                    ..fake_connection_stats(seed)
6193                }),
6194                ..Default::default()
6195            })
6196        }));
6197
6198        test_helper.send_connected_event(random_bss_description!(Wpa2));
6199        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6200
6201        // Verify there's no crash
6202        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6203        // Verify that counters are not incremented
6204        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
6205            stats: contains {
6206                get_iface_stats_fail_count: 0u64,
6207                "1d_counters": contains {
6208                    // Deduct 15 seconds because there isn't packet counter to diff against in
6209                    // the first interval of telemetry
6210                    rx_high_packet_drop_duration: 0i64,
6211                    tx_high_packet_drop_duration: 0i64,
6212                    // Very high drop rate counters should still be 0
6213                    rx_very_high_packet_drop_duration: 0i64,
6214                    tx_very_high_packet_drop_duration: 0i64,
6215                    no_rx_duration: 0i64,
6216                },
6217                "7d_counters": contains {
6218                    rx_high_packet_drop_duration: 0i64,
6219                    tx_high_packet_drop_duration: 0i64,
6220                    rx_very_high_packet_drop_duration: 0i64,
6221                    tx_very_high_packet_drop_duration: 0i64,
6222                    no_rx_duration: 0i64,
6223                },
6224            }
6225        });
6226    }
6227
6228    #[fuchsia::test]
6229    fn test_rx_tx_packet_time_series() {
6230        let (mut test_helper, mut test_fut) = setup_test();
6231        test_helper.set_iface_stats_resp(Box::new(|| {
6232            let seed = (fasync::MonotonicInstant::now()
6233                - fasync::MonotonicInstant::from_nanos(0i64))
6234            .into_seconds() as u64;
6235            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6236                connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6237                    rx_unicast_total: Some(100 * seed),
6238                    rx_unicast_drop: Some(3 * seed),
6239                    tx_total: Some(10 * seed),
6240                    tx_drop: Some(2 * seed),
6241                    ..fake_connection_stats(seed)
6242                }),
6243                ..Default::default()
6244            })
6245        }));
6246
6247        test_helper.send_connected_event(random_bss_description!(Wpa2));
6248        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6249
6250        test_helper.advance_by(zx::MonotonicDuration::from_minutes(2), test_fut.as_mut());
6251
6252        let time_series = test_helper.get_time_series(&mut test_fut);
6253        let rx_unicast_drop_count: Vec<_> =
6254            time_series.lock().rx_unicast_drop_count.minutely_iter().copied().collect();
6255        let rx_unicast_total_count: Vec<_> =
6256            time_series.lock().rx_unicast_total_count.minutely_iter().copied().collect();
6257        let tx_drop_count: Vec<_> =
6258            time_series.lock().tx_drop_count.minutely_iter().copied().collect();
6259        let tx_total_count: Vec<_> =
6260            time_series.lock().tx_total_count.minutely_iter().copied().collect();
6261
6262        // Note: Packets from the first 15 seconds are not accounted because we
6263        //       we did not take packet measurement at 0th second mark.
6264        //       Additionally, the count for 45th-60th second mark is logged
6265        //       at the 60th mark, which is considered to be part of the second
6266        //       window.
6267        assert_eq!(rx_unicast_drop_count, vec![90, 180, 45]);
6268        assert_eq!(rx_unicast_total_count, vec![3000, 6000, 1500]);
6269        assert_eq!(tx_drop_count, vec![60, 120, 30]);
6270        assert_eq!(tx_total_count, vec![300, 600, 150]);
6271    }
6272
6273    #[fuchsia::test]
6274    fn test_no_rx_duration_counters() {
6275        let (mut test_helper, mut test_fut) = setup_test();
6276        test_helper.set_iface_stats_resp(Box::new(|| {
6277            let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
6278            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6279                connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6280                    rx_unicast_total: Some(10),
6281                    ..fake_connection_stats(seed)
6282                }),
6283                ..Default::default()
6284            })
6285        }));
6286
6287        test_helper.send_connected_event(random_bss_description!(Wpa2));
6288        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6289
6290        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6291        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
6292            stats: contains {
6293                get_iface_stats_fail_count: 0u64,
6294                "1d_counters": contains {
6295                    // Deduct 15 seconds beecause there isn't packet counter to diff against in
6296                    // the first interval of telemetry
6297                    no_rx_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6298                    rx_high_packet_drop_duration: 0i64,
6299                    tx_high_packet_drop_duration: 0i64,
6300                    rx_very_high_packet_drop_duration: 0i64,
6301                    tx_very_high_packet_drop_duration: 0i64,
6302                },
6303                "7d_counters": contains {
6304                    no_rx_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6305                    rx_high_packet_drop_duration: 0i64,
6306                    tx_high_packet_drop_duration: 0i64,
6307                    rx_very_high_packet_drop_duration: 0i64,
6308                    tx_very_high_packet_drop_duration: 0i64,
6309                },
6310            }
6311        });
6312    }
6313
6314    #[fuchsia::test]
6315    fn test_no_rx_duration_time_series() {
6316        let (mut test_helper, mut test_fut) = setup_test();
6317        test_helper.set_iface_stats_resp(Box::new(|| {
6318            let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
6319            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6320                connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6321                    rx_unicast_total: Some(10),
6322                    ..fake_connection_stats(seed)
6323                }),
6324                ..Default::default()
6325            })
6326        }));
6327
6328        test_helper.send_connected_event(random_bss_description!(Wpa2));
6329        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6330
6331        test_helper.advance_by(zx::MonotonicDuration::from_seconds(150), test_fut.as_mut());
6332        let time_series = test_helper.get_time_series(&mut test_fut);
6333        let no_rx_duration_sec: Vec<_> =
6334            time_series.lock().no_rx_duration_sec.minutely_iter().copied().collect();
6335        assert_eq!(no_rx_duration_sec, vec![30, 60, 45]);
6336    }
6337
6338    #[fuchsia::test]
6339    fn test_get_iface_stats_fail() {
6340        let (mut test_helper, mut test_fut) = setup_test();
6341        test_helper.set_iface_stats_resp(Box::new(|| Err(zx::sys::ZX_ERR_NOT_SUPPORTED)));
6342
6343        test_helper.send_connected_event(random_bss_description!(Wpa2));
6344        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6345
6346        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6347        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
6348            stats: contains {
6349                get_iface_stats_fail_count: NonZeroUintProperty,
6350                "1d_counters": contains {
6351                    no_rx_duration: 0i64,
6352                    rx_high_packet_drop_duration: 0i64,
6353                    tx_high_packet_drop_duration: 0i64,
6354                    rx_very_high_packet_drop_duration: 0i64,
6355                    tx_very_high_packet_drop_duration: 0i64,
6356                },
6357                "7d_counters": contains {
6358                    no_rx_duration: 0i64,
6359                    rx_high_packet_drop_duration: 0i64,
6360                    tx_high_packet_drop_duration: 0i64,
6361                    rx_very_high_packet_drop_duration: 0i64,
6362                    tx_very_high_packet_drop_duration: 0i64,
6363                },
6364            }
6365        });
6366    }
6367
6368    #[fuchsia::test]
6369    fn test_log_signal_histograms_inspect() {
6370        let (mut test_helper, mut test_fut) = setup_test();
6371        test_helper.send_connected_event(random_bss_description!(Wpa2));
6372        test_helper.drain_cobalt_events(&mut test_fut);
6373
6374        // Default iface stats responder in `test_helper` already mock these histograms.
6375        assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
6376            external: contains {
6377                stats: contains {
6378                    connection_status: contains {
6379                        histograms: {
6380                            antenna0_2Ghz: {
6381                                antenna_index: 0u64,
6382                                antenna_freq: "2Ghz",
6383                                snr_histogram: vec![30i64, 999],
6384                                snr_invalid_samples: 11u64,
6385                                noise_floor_histogram: vec![-55i64, 999],
6386                                noise_floor_invalid_samples: 44u64,
6387                                rssi_histogram: vec![-25i64, 999],
6388                                rssi_invalid_samples: 55u64,
6389                            },
6390                            antenna1_5Ghz: {
6391                                antenna_index: 1u64,
6392                                antenna_freq: "5Ghz",
6393                                rx_rate_histogram: vec![100i64, 1500],
6394                                rx_rate_invalid_samples: 33u64,
6395                            },
6396                        }
6397                    }
6398                }
6399            }
6400        });
6401    }
6402
6403    #[fuchsia::test]
6404    fn test_log_daily_uptime_ratio_cobalt_metric() {
6405        let (mut test_helper, mut test_fut) = setup_test();
6406        test_helper.send_connected_event(random_bss_description!(Wpa2));
6407        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6408
6409        test_helper.advance_by(zx::MonotonicDuration::from_hours(12), test_fut.as_mut());
6410
6411        let info = fake_disconnect_info();
6412        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6413            track_subsequent_downtime: true,
6414            info: Some(info),
6415        });
6416        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6417
6418        test_helper.advance_by(zx::MonotonicDuration::from_hours(6), test_fut.as_mut());
6419
6420        // Indicate that there's no saved neighbor in vicinity
6421        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
6422            network_selection_type: NetworkSelectionType::Undirected,
6423            num_candidates: Ok(0),
6424            selected_count: 0,
6425        });
6426
6427        test_helper.advance_by(zx::MonotonicDuration::from_hours(6), test_fut.as_mut());
6428
6429        let uptime_ratios =
6430            test_helper.get_logged_metrics(metrics::CONNECTED_UPTIME_RATIO_METRIC_ID);
6431        assert_eq!(uptime_ratios.len(), 1);
6432        // 12 hours of uptime, 6 hours of adjusted downtime => 66.66% uptime
6433        assert_eq!(uptime_ratios[0].payload, MetricEventPayload::IntegerValue(6666));
6434    }
6435
6436    /// Send a random connect event and 4 hours later send a disconnect with the specified
6437    /// disconnect source.
6438    fn connect_and_disconnect_with_source(
6439        test_helper: &mut TestHelper,
6440        mut test_fut: Pin<&mut impl Future<Output = ()>>,
6441        disconnect_source: fidl_sme::DisconnectSource,
6442    ) {
6443        test_helper.send_connected_event(random_bss_description!(Wpa2));
6444        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6445
6446        test_helper.advance_by(zx::MonotonicDuration::from_hours(6), test_fut.as_mut());
6447
6448        let info = DisconnectInfo { disconnect_source, ..fake_disconnect_info() };
6449        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6450            track_subsequent_downtime: true,
6451            info: Some(info),
6452        });
6453        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6454        test_helper.drain_cobalt_events(&mut test_fut);
6455    }
6456
6457    #[fuchsia::test]
6458    fn test_log_daily_disconnect_per_day_connected_cobalt_metric() {
6459        let (mut test_helper, mut test_fut) = setup_test();
6460
6461        // Send 1 disconnect and 1 roaming disconnect with the device connected for a
6462        // total of 12 of 24 hours.
6463        let mlme_non_roam_source = fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
6464            reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
6465            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
6466        });
6467        connect_and_disconnect_with_source(
6468            &mut test_helper,
6469            test_fut.as_mut(),
6470            mlme_non_roam_source,
6471        );
6472
6473        let mlme_roam_source = fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
6474            reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
6475            mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamConfirmation,
6476        });
6477        connect_and_disconnect_with_source(&mut test_helper, test_fut.as_mut(), mlme_roam_source);
6478
6479        test_helper.advance_by(zx::MonotonicDuration::from_hours(12), test_fut.as_mut());
6480
6481        let dpdc_ratios =
6482            test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6483        assert_eq!(dpdc_ratios.len(), 1);
6484        // 2 disconnects, 0.5 day connected => 4 disconnects per day connected
6485        assert_eq!(dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(40_000));
6486
6487        // 1 non-roaming disconnect, 0.5 day connected => 2 non-roam disconnects per day connected
6488        let non_roam_dpdc_ratios = test_helper
6489            .get_logged_metrics(metrics::NON_ROAM_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6490        assert_eq!(non_roam_dpdc_ratios.len(), 1);
6491        assert_eq!(non_roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(20_000));
6492
6493        // Roam disconnects get logged in the roam result event, so this shouldn't have any, even
6494        // though we directly sent the roam disconnect..
6495        let roam_dpdc_ratios = test_helper
6496            .get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_PER_DAY_CONNECTED_METRIC_ID);
6497        assert_eq!(roam_dpdc_ratios.len(), 1);
6498        assert_eq!(roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6499
6500        let dpdc_ratios_7d =
6501            test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_7D_METRIC_ID);
6502        assert_eq!(dpdc_ratios_7d.len(), 1);
6503        assert_eq!(dpdc_ratios_7d[0].payload, MetricEventPayload::IntegerValue(40_000));
6504
6505        // Clear record of logged Cobalt events
6506        test_helper.cobalt_events.clear();
6507
6508        // Connect for another 1 day to dilute the 7d ratio
6509        test_helper.send_connected_event(random_bss_description!(Wpa2));
6510        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6511
6512        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
6513
6514        // No disconnect in the last day, so the 1d ratio would be 0 for all types.
6515        let dpdc_ratios =
6516            test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6517        assert_eq!(dpdc_ratios.len(), 1);
6518        assert_eq!(dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6519
6520        let non_roam_dpdc_ratios = test_helper
6521            .get_logged_metrics(metrics::NON_ROAM_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6522        assert_eq!(non_roam_dpdc_ratios.len(), 1);
6523        assert_eq!(non_roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6524
6525        let roam_dpdc_ratios = test_helper
6526            .get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_PER_DAY_CONNECTED_METRIC_ID);
6527        assert_eq!(roam_dpdc_ratios.len(), 1);
6528        assert_eq!(roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6529
6530        let dpdc_ratios_7d =
6531            test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_7D_METRIC_ID);
6532        assert_eq!(dpdc_ratios_7d.len(), 1);
6533        // The original 2 disconnects, now with 1.5 day connected => 1.333 disconnects per day
6534        // connected (which equals 13,333 in TenThousandth unit)
6535        assert_eq!(dpdc_ratios_7d[0].payload, MetricEventPayload::IntegerValue(13_333));
6536    }
6537
6538    #[fuchsia::test]
6539    fn test_log_daily_policy_roam_disconnect_per_day_connected_cobalt_metric() {
6540        let (mut test_helper, mut test_fut) = setup_test();
6541        test_helper.send_connected_event(random_bss_description!(Wpa2));
6542        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6543
6544        // Send one successful roam result
6545        let bss_desc = random_fidl_bss_description!();
6546        let roam_result = fidl_sme::RoamResult {
6547            bssid: [1, 1, 1, 1, 1, 1],
6548            status_code: fidl_ieee80211::StatusCode::Success,
6549            original_association_maintained: false,
6550            bss_description: Some(Box::new(bss_desc.clone())),
6551            disconnect_info: None,
6552            is_credential_rejected: false,
6553        };
6554        test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
6555            iface_id: 1,
6556            result: roam_result,
6557            updated_ap_state: generate_random_ap_state(),
6558            original_ap_state: Box::new(generate_random_ap_state()),
6559            request: Box::new(generate_policy_roam_request([1, 1, 1, 1, 1, 1].into())),
6560            request_time: fasync::MonotonicInstant::now(),
6561            result_time: fasync::MonotonicInstant::now(),
6562        });
6563        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6564        test_helper.advance_by(zx::MonotonicDuration::from_hours(12), test_fut.as_mut());
6565
6566        // Send a second successful roam result
6567        let bss_desc = random_fidl_bss_description!();
6568        let roam_result = fidl_sme::RoamResult {
6569            bssid: [2, 2, 2, 2, 2, 2],
6570            status_code: fidl_ieee80211::StatusCode::Success,
6571            original_association_maintained: false,
6572            bss_description: Some(Box::new(bss_desc.clone())),
6573            disconnect_info: None,
6574            is_credential_rejected: false,
6575        };
6576        test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
6577            iface_id: 1,
6578            result: roam_result,
6579            updated_ap_state: generate_random_ap_state(),
6580            original_ap_state: Box::new(generate_random_ap_state()),
6581            request: Box::new(generate_policy_roam_request([2, 2, 2, 2, 2, 2].into())),
6582            request_time: fasync::MonotonicInstant::now(),
6583            result_time: fasync::MonotonicInstant::now(),
6584        });
6585        // Send a disconnect
6586        let info = DisconnectInfo {
6587            disconnect_source: fidl_sme::DisconnectSource::User(
6588                fidl_sme::UserDisconnectReason::Unknown,
6589            ),
6590            ..fake_disconnect_info()
6591        };
6592        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6593            track_subsequent_downtime: true,
6594            info: Some(info),
6595        });
6596        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6597        test_helper.advance_by(zx::MonotonicDuration::from_hours(12), test_fut.as_mut());
6598
6599        let dpdc_ratios =
6600            test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6601        assert_eq!(dpdc_ratios.len(), 1);
6602        // 1 disconnect, 0.5 day connected => 2 disconnects per day connected
6603        // (which equals 20_0000 in TenThousandth unit)
6604        assert_eq!(dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(20_000));
6605
6606        // 2 roam disconnects, 0.4 day connected => 4 roam disconnects per day connected
6607        let roam_dpdc_ratios = test_helper
6608            .get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_PER_DAY_CONNECTED_METRIC_ID);
6609        assert_eq!(roam_dpdc_ratios.len(), 1);
6610        assert_eq!(roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(40_000));
6611    }
6612
6613    #[fuchsia::test]
6614    fn test_log_daily_disconnect_per_day_connected_cobalt_metric_device_high_disconnect() {
6615        let (mut test_helper, mut test_fut) = setup_test();
6616        test_helper.send_connected_event(random_bss_description!(Wpa2));
6617        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6618
6619        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6620        let info = fake_disconnect_info();
6621        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6622            track_subsequent_downtime: true,
6623            info: Some(info),
6624        });
6625        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6626
6627        test_helper.advance_by(zx::MonotonicDuration::from_hours(23), test_fut.as_mut());
6628    }
6629
6630    #[fuchsia::test]
6631    fn test_log_daily_rx_tx_ratio_cobalt_metrics() {
6632        let (mut test_helper, mut test_fut) = setup_test();
6633        test_helper.set_iface_stats_resp(Box::new(|| {
6634            let seed = fasync::MonotonicInstant::now().into_nanos() as u64 / 1_000_000_000;
6635            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6636                connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6637                    tx_total: Some(10 * seed),
6638                    // TX drop rate stops increasing at 1 hour + TELEMETRY_QUERY_INTERVAL mark.
6639                    // Because the first TELEMETRY_QUERY_INTERVAL doesn't count when
6640                    // computing counters, this leads to 3 hour of high TX drop rate.
6641                    tx_drop: Some(
6642                        3 * min(
6643                            seed,
6644                            (zx::MonotonicDuration::from_hours(3) + TELEMETRY_QUERY_INTERVAL)
6645                                .into_seconds() as u64,
6646                        ),
6647                    ),
6648                    // RX total stops increasing at 23 hour mark
6649                    rx_unicast_total: Some(
6650                        10 * min(seed, zx::MonotonicDuration::from_hours(23).into_seconds() as u64),
6651                    ),
6652                    // RX drop rate stops increasing at 4 hour + TELEMETRY_QUERY_INTERVAL mark.
6653                    rx_unicast_drop: Some(
6654                        3 * min(
6655                            seed,
6656                            (zx::MonotonicDuration::from_hours(4) + TELEMETRY_QUERY_INTERVAL)
6657                                .into_seconds() as u64,
6658                        ),
6659                    ),
6660                    ..fake_connection_stats(seed)
6661                }),
6662                ..Default::default()
6663            })
6664        }));
6665
6666        test_helper.send_connected_event(random_bss_description!(Wpa2));
6667        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6668
6669        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
6670
6671        let high_rx_drop_time_ratios =
6672            test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_RX_PACKET_DROP_METRIC_ID);
6673        // 4 hours of high RX drop rate, 24 hours connected => 16.66% duration
6674        assert_eq!(high_rx_drop_time_ratios.len(), 1);
6675        assert_eq!(high_rx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(1666));
6676
6677        let high_tx_drop_time_ratios =
6678            test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_TX_PACKET_DROP_METRIC_ID);
6679        // 3 hours of high RX drop rate, 24 hours connected => 12.48% duration
6680        assert_eq!(high_tx_drop_time_ratios.len(), 1);
6681        assert_eq!(high_tx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(1250));
6682
6683        let very_high_rx_drop_time_ratios = test_helper
6684            .get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID);
6685        assert_eq!(very_high_rx_drop_time_ratios.len(), 1);
6686        assert_eq!(
6687            very_high_rx_drop_time_ratios[0].payload,
6688            MetricEventPayload::IntegerValue(1666)
6689        );
6690
6691        let very_high_tx_drop_time_ratios = test_helper
6692            .get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID);
6693        assert_eq!(very_high_tx_drop_time_ratios.len(), 1);
6694        assert_eq!(
6695            very_high_tx_drop_time_ratios[0].payload,
6696            MetricEventPayload::IntegerValue(1250)
6697        );
6698
6699        // 1 hour of no RX, 24 hours connected => 4.16% duration
6700        let no_rx_time_ratios =
6701            test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_NO_RX_METRIC_ID);
6702        assert_eq!(no_rx_time_ratios.len(), 1);
6703        assert_eq!(no_rx_time_ratios[0].payload, MetricEventPayload::IntegerValue(416));
6704    }
6705
6706    #[fuchsia::test]
6707    fn test_log_daily_rx_tx_ratio_cobalt_metrics_zero() {
6708        // This test is to verify that when the RX/TX ratios are 0 (there's no issue), we still
6709        // log to Cobalt.
6710        let (mut test_helper, mut test_fut) = setup_test();
6711
6712        test_helper.send_connected_event(random_bss_description!(Wpa2));
6713        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6714
6715        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
6716
6717        let high_rx_drop_time_ratios =
6718            test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_RX_PACKET_DROP_METRIC_ID);
6719        assert_eq!(high_rx_drop_time_ratios.len(), 1);
6720        assert_eq!(high_rx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6721
6722        let high_tx_drop_time_ratios =
6723            test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_TX_PACKET_DROP_METRIC_ID);
6724        assert_eq!(high_tx_drop_time_ratios.len(), 1);
6725        assert_eq!(high_tx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6726
6727        let very_high_rx_drop_time_ratios = test_helper
6728            .get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID);
6729        assert_eq!(very_high_rx_drop_time_ratios.len(), 1);
6730        assert_eq!(very_high_rx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6731
6732        let very_high_tx_drop_time_ratios = test_helper
6733            .get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID);
6734        assert_eq!(very_high_tx_drop_time_ratios.len(), 1);
6735        assert_eq!(very_high_tx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6736
6737        let no_rx_time_ratios =
6738            test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_NO_RX_METRIC_ID);
6739        assert_eq!(no_rx_time_ratios.len(), 1);
6740        assert_eq!(no_rx_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6741    }
6742
6743    #[fuchsia::test]
6744    fn test_log_daily_establish_connection_metrics() {
6745        let (mut test_helper, mut test_fut) = setup_test();
6746
6747        // Send 10 failed connect results, then 1 successful.
6748        for _ in 0..10 {
6749            let event = TelemetryEvent::ConnectResult {
6750                iface_id: IFACE_ID,
6751                policy_connect_reason: Some(
6752                    client::types::ConnectReason::RetryAfterFailedConnectAttempt,
6753                ),
6754                result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
6755                multiple_bss_candidates: true,
6756                ap_state: random_bss_description!(Wpa1).into(),
6757                network_is_likely_hidden: true,
6758            };
6759            test_helper.telemetry_sender.send(event);
6760        }
6761        test_helper.send_connected_event(random_bss_description!(Wpa2));
6762
6763        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
6764
6765        let connection_success_rate =
6766            test_helper.get_logged_metrics(metrics::CONNECTION_SUCCESS_RATE_METRIC_ID);
6767        assert_eq!(connection_success_rate.len(), 1);
6768        // 1 successful, 11 total attempts => 9.09% success rate
6769        assert_eq!(connection_success_rate[0].payload, MetricEventPayload::IntegerValue(909));
6770    }
6771
6772    #[fuchsia::test]
6773    fn test_log_hourly_fleetwide_uptime_cobalt_metrics() {
6774        let (mut test_helper, mut test_fut) = setup_test();
6775
6776        test_helper.send_connected_event(random_bss_description!(Wpa2));
6777        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6778
6779        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6780
6781        let total_wlan_uptime_durs =
6782            test_helper.get_logged_metrics(metrics::TOTAL_WLAN_UPTIME_NEAR_SAVED_NETWORK_METRIC_ID);
6783        assert_eq!(total_wlan_uptime_durs.len(), 1);
6784        assert_eq!(
6785            total_wlan_uptime_durs[0].payload,
6786            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_hours(1).into_micros())
6787        );
6788
6789        let connected_durs =
6790            test_helper.get_logged_metrics(metrics::TOTAL_CONNECTED_UPTIME_METRIC_ID);
6791        assert_eq!(connected_durs.len(), 1);
6792        assert_eq!(
6793            connected_durs[0].payload,
6794            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_hours(1).into_micros())
6795        );
6796
6797        // Clear record of logged Cobalt events
6798        test_helper.cobalt_events.clear();
6799
6800        test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
6801
6802        let info = fake_disconnect_info();
6803        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6804            track_subsequent_downtime: true,
6805            info: Some(info),
6806        });
6807        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6808
6809        test_helper.advance_by(zx::MonotonicDuration::from_minutes(15), test_fut.as_mut());
6810
6811        // Indicate that there's no saved neighbor in vicinity
6812        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
6813            network_selection_type: NetworkSelectionType::Undirected,
6814            num_candidates: Ok(0),
6815            selected_count: 0,
6816        });
6817        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6818
6819        test_helper.advance_by(zx::MonotonicDuration::from_minutes(15), test_fut.as_mut());
6820
6821        let total_wlan_uptime_durs =
6822            test_helper.get_logged_metrics(metrics::TOTAL_WLAN_UPTIME_NEAR_SAVED_NETWORK_METRIC_ID);
6823        assert_eq!(total_wlan_uptime_durs.len(), 1);
6824        // 30 minutes connected uptime + 15 minutes downtime near saved network
6825        assert_eq!(
6826            total_wlan_uptime_durs[0].payload,
6827            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(45).into_micros())
6828        );
6829
6830        let connected_durs =
6831            test_helper.get_logged_metrics(metrics::TOTAL_CONNECTED_UPTIME_METRIC_ID);
6832        assert_eq!(connected_durs.len(), 1);
6833        assert_eq!(
6834            connected_durs[0].payload,
6835            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(30).into_micros())
6836        );
6837    }
6838
6839    #[fuchsia::test]
6840    fn test_log_hourly_fleetwide_rx_tx_cobalt_metrics() {
6841        let (mut test_helper, mut test_fut) = setup_test();
6842        test_helper.set_iface_stats_resp(Box::new(|| {
6843            let seed = fasync::MonotonicInstant::now().into_nanos() as u64 / 1_000_000_000;
6844            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6845                connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6846                    tx_total: Some(10 * seed),
6847                    // TX drop rate stops increasing at 10 min + TELEMETRY_QUERY_INTERVAL mark.
6848                    // Because the first TELEMETRY_QUERY_INTERVAL doesn't count when
6849                    // computing counters, this leads to 10 min of high TX drop rate.
6850                    tx_drop: Some(
6851                        3 * min(
6852                            seed,
6853                            (zx::MonotonicDuration::from_minutes(10) + TELEMETRY_QUERY_INTERVAL)
6854                                .into_seconds() as u64,
6855                        ),
6856                    ),
6857                    // RX total stops increasing at 45 min mark
6858                    rx_unicast_total: Some(
6859                        10 * min(
6860                            seed,
6861                            zx::MonotonicDuration::from_minutes(45).into_seconds() as u64,
6862                        ),
6863                    ),
6864                    // RX drop rate stops increasing at 20 min + TELEMETRY_QUERY_INTERVAL mark.
6865                    rx_unicast_drop: Some(
6866                        3 * min(
6867                            seed,
6868                            (zx::MonotonicDuration::from_minutes(20) + TELEMETRY_QUERY_INTERVAL)
6869                                .into_seconds() as u64,
6870                        ),
6871                    ),
6872                    ..fake_connection_stats(seed)
6873                }),
6874                ..Default::default()
6875            })
6876        }));
6877
6878        test_helper.send_connected_event(random_bss_description!(Wpa2));
6879        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6880
6881        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6882
6883        let rx_high_drop_durs =
6884            test_helper.get_logged_metrics(metrics::TOTAL_TIME_WITH_HIGH_RX_PACKET_DROP_METRIC_ID);
6885        assert_eq!(rx_high_drop_durs.len(), 1);
6886        assert_eq!(
6887            rx_high_drop_durs[0].payload,
6888            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(20).into_micros())
6889        );
6890
6891        let tx_high_drop_durs =
6892            test_helper.get_logged_metrics(metrics::TOTAL_TIME_WITH_HIGH_TX_PACKET_DROP_METRIC_ID);
6893        assert_eq!(tx_high_drop_durs.len(), 1);
6894        assert_eq!(
6895            tx_high_drop_durs[0].payload,
6896            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(10).into_micros())
6897        );
6898
6899        let rx_very_high_drop_durs = test_helper
6900            .get_logged_metrics(metrics::TOTAL_TIME_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID);
6901        assert_eq!(rx_very_high_drop_durs.len(), 1);
6902        assert_eq!(
6903            rx_very_high_drop_durs[0].payload,
6904            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(20).into_micros())
6905        );
6906
6907        let tx_very_high_drop_durs = test_helper
6908            .get_logged_metrics(metrics::TOTAL_TIME_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID);
6909        assert_eq!(tx_very_high_drop_durs.len(), 1);
6910        assert_eq!(
6911            tx_very_high_drop_durs[0].payload,
6912            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(10).into_micros())
6913        );
6914
6915        let no_rx_durs = test_helper.get_logged_metrics(metrics::TOTAL_TIME_WITH_NO_RX_METRIC_ID);
6916        assert_eq!(no_rx_durs.len(), 1);
6917        assert_eq!(
6918            no_rx_durs[0].payload,
6919            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(15).into_micros())
6920        );
6921    }
6922
6923    #[fuchsia::test]
6924    fn test_log_rssi_hourly() {
6925        let (mut test_helper, mut test_fut) = setup_test();
6926
6927        // RSSI velocity is only logged if in the connected state.
6928        test_helper.send_connected_event(random_bss_description!(Wpa2));
6929
6930        // Send some RSSI velocities
6931        let ind_1 = fidl_internal::SignalReportIndication { rssi_dbm: -50, snr_db: 30 };
6932        let ind_2 = fidl_internal::SignalReportIndication { rssi_dbm: -61, snr_db: 40 };
6933        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_1 });
6934        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_1 });
6935        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_2 });
6936
6937        // After an hour has passed, the RSSI should be logged to cobalt
6938        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6939        test_helper.drain_cobalt_events(&mut test_fut);
6940
6941        let metrics = test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_METRIC_ID);
6942        assert_eq!(metrics.len(), 1);
6943        assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => {
6944            assert_eq!(buckets.len(), 2);
6945            assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 79, count: 2}));
6946            assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 68, count: 1}));
6947        });
6948        test_helper.clear_cobalt_events();
6949
6950        // Send another different RSSI
6951        let ind_3 = fidl_internal::SignalReportIndication { rssi_dbm: -75, snr_db: 30 };
6952        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_3 });
6953        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6954
6955        // Check that the previously logged values are not logged again, and the new value is
6956        // logged.
6957        test_helper.drain_cobalt_events(&mut test_fut);
6958
6959        let metrics = test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_METRIC_ID);
6960        assert_eq!(metrics.len(), 1);
6961        let buckets =
6962            assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => buckets);
6963        assert_eq!(buckets.len(), 1);
6964        assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 54, count: 1 }));
6965    }
6966
6967    #[fuchsia::test]
6968    fn test_log_rssi_velocity_hourly() {
6969        let (mut test_helper, mut test_fut) = setup_test();
6970
6971        // RSSI velocity is only logged if in the connected state.
6972        test_helper.send_connected_event(random_bss_description!(Wpa2));
6973
6974        // Send some RSSI velocities
6975        let rssi_velocity_1 = -2.0;
6976        let rssi_velocity_2 = 2.0;
6977        test_helper
6978            .telemetry_sender
6979            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: rssi_velocity_1 });
6980        test_helper
6981            .telemetry_sender
6982            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: rssi_velocity_2 });
6983        test_helper
6984            .telemetry_sender
6985            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: rssi_velocity_2 });
6986
6987        // After an hour has passed, the RSSI velocity should be logged to cobalt
6988        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6989        test_helper.drain_cobalt_events(&mut test_fut);
6990
6991        let metrics = test_helper.get_logged_metrics(metrics::RSSI_VELOCITY_METRIC_ID);
6992        assert_eq!(metrics.len(), 1);
6993        assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => {
6994            // RSSI velocity in [-2,-1) maps to bucket 9 and velocity in [2,3) maps to bucket 13.
6995            assert_eq!(buckets.len(), 2);
6996            assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 9, count: 1}));
6997            assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 13, count: 2}));
6998        });
6999        test_helper.clear_cobalt_events();
7000
7001        // Send another different RSSI velocity
7002        let rssi_velocity_3 = 3.0;
7003        test_helper
7004            .telemetry_sender
7005            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: rssi_velocity_3 });
7006        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
7007
7008        // Check that the previously logged values are not logged again, and the new value is
7009        // logged.
7010        test_helper.drain_cobalt_events(&mut test_fut);
7011
7012        let metrics = test_helper.get_logged_metrics(metrics::RSSI_VELOCITY_METRIC_ID);
7013        assert_eq!(metrics.len(), 1);
7014        assert_eq!(
7015            metrics[0].payload,
7016            MetricEventPayload::Histogram(vec![fidl_fuchsia_metrics::HistogramBucket {
7017                index: 14,
7018                count: 1
7019            }])
7020        );
7021    }
7022
7023    #[fuchsia::test]
7024    fn test_log_rssi_histogram_bounds() {
7025        let (mut test_helper, mut test_fut) = setup_test();
7026
7027        // RSSI is only logged if in the connected state.
7028        test_helper.send_connected_event(random_bss_description!(Wpa2));
7029
7030        let ind_min = fidl_internal::SignalReportIndication { rssi_dbm: -128, snr_db: 30 };
7031        // 0 is the highest histogram bucket and 1 and above are in the overflow bucket.
7032        let ind_max = fidl_internal::SignalReportIndication { rssi_dbm: 0, snr_db: 30 };
7033        let ind_overflow_1 = fidl_internal::SignalReportIndication { rssi_dbm: 1, snr_db: 30 };
7034        let ind_overflow_2 = fidl_internal::SignalReportIndication { rssi_dbm: 127, snr_db: 30 };
7035        // Send the telemetry events. -10 is the min velocity bucket and 10 is the max.
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_min });
7039        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_max });
7040        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_overflow_1 });
7041        test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_overflow_2 });
7042        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
7043
7044        // Check that the min, max, underflow, and overflow buckets are used correctly.
7045        test_helper.drain_cobalt_events(&mut test_fut);
7046        // Check RSSI values
7047        let metrics = test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_METRIC_ID);
7048        assert_eq!(metrics.len(), 1);
7049        let buckets =
7050            assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => buckets);
7051        assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 1, count: 3 }));
7052        assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 129, count: 1 }));
7053        assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 130, count: 2 }));
7054    }
7055
7056    #[fuchsia::test]
7057    fn test_log_rssi_velocity_histogram_bounds() {
7058        let (mut test_helper, mut test_fut) = setup_test();
7059
7060        // RSSI velocity is only logged if in the connected state.
7061        test_helper.send_connected_event(random_bss_description!(Wpa2));
7062
7063        // Send the telemetry events. -10 is the min velocity bucket and 10 is the max.
7064        test_helper
7065            .telemetry_sender
7066            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: -11.0 });
7067        test_helper
7068            .telemetry_sender
7069            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: -15.0 });
7070        test_helper
7071            .telemetry_sender
7072            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: 11.0 });
7073        test_helper
7074            .telemetry_sender
7075            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: 20.0 });
7076        test_helper
7077            .telemetry_sender
7078            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: -10.0 });
7079        test_helper
7080            .telemetry_sender
7081            .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: 10.0 });
7082        test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
7083
7084        // Check that the min, max, underflow, and overflow buckets are used correctly.
7085        test_helper.drain_cobalt_events(&mut test_fut);
7086
7087        // Check RSSI velocity values
7088        let metrics = test_helper.get_logged_metrics(metrics::RSSI_VELOCITY_METRIC_ID);
7089        assert_eq!(metrics.len(), 1);
7090        let buckets =
7091            assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => buckets);
7092        // RSSI velocity below -10 maps to underflow bucket, and 11 or above maps to overflow.
7093        assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 1, count: 1 }));
7094        assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 21, count: 1 }));
7095        assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 0, count: 2 }));
7096        assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 22, count: 2 }));
7097    }
7098
7099    #[fuchsia::test]
7100    fn test_log_short_duration_connection_metrics() {
7101        let (mut test_helper, mut test_fut) = setup_test();
7102        let now = fasync::MonotonicInstant::now();
7103        test_helper.send_connected_event(random_bss_description!(Wpa2));
7104        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7105
7106        let channel = generate_random_channel();
7107        let ap_state = random_bss_description!(Wpa2, channel: channel).into();
7108        let mut signals = HistoricalList::new(5);
7109        signals.add(client::types::TimestampedSignal {
7110            signal: client::types::Signal { rssi_dbm: -30, snr_db: 60 },
7111            time: now,
7112        });
7113        signals.add(client::types::TimestampedSignal {
7114            signal: client::types::Signal { rssi_dbm: -30, snr_db: 60 },
7115            time: now,
7116        });
7117        // Log disconnect with reason FidlConnectRequest during short duration
7118        let info = DisconnectInfo {
7119            connected_duration: METRICS_SHORT_CONNECT_DURATION
7120                - zx::MonotonicDuration::from_seconds(1),
7121            disconnect_source: fidl_sme::DisconnectSource::User(
7122                fidl_sme::UserDisconnectReason::FidlConnectRequest,
7123            ),
7124            ap_state,
7125            signals,
7126            ..fake_disconnect_info()
7127        };
7128        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7129            track_subsequent_downtime: true,
7130            info: Some(info.clone()),
7131        });
7132
7133        test_helper.send_connected_event(random_bss_description!(Wpa2));
7134        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7135
7136        // Log disconnect with reason NetworkUnsaved during short duration
7137        let info = DisconnectInfo {
7138            disconnect_source: fidl_sme::DisconnectSource::User(
7139                fidl_sme::UserDisconnectReason::NetworkUnsaved,
7140            ),
7141            ..info
7142        };
7143        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7144            track_subsequent_downtime: true,
7145            info: Some(info.clone()),
7146        });
7147
7148        test_helper.send_connected_event(random_bss_description!(Wpa2));
7149        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7150
7151        // Log disconnect with reason NetworkUnsaved during longer duration connection
7152        let info = DisconnectInfo {
7153            connected_duration: METRICS_SHORT_CONNECT_DURATION
7154                + zx::MonotonicDuration::from_seconds(1),
7155            ..info
7156        };
7157        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7158            track_subsequent_downtime: true,
7159            info: Some(info.clone()),
7160        });
7161
7162        test_helper.drain_cobalt_events(&mut test_fut);
7163
7164        let logged_metrics = test_helper.get_logged_metrics(
7165            metrics::POLICY_FIDL_CONNECTION_ATTEMPTS_DURING_SHORT_CONNECTION_METRIC_ID,
7166        );
7167        assert_eq!(logged_metrics.len(), 2);
7168
7169        let logged_metrics = test_helper.get_logged_metrics(
7170            metrics::POLICY_FIDL_CONNECTION_ATTEMPTS_DURING_SHORT_CONNECTION_DETAILED_METRIC_ID,
7171        );
7172        assert_eq!(logged_metrics.len(), 2);
7173        assert_eq!(logged_metrics[0].event_codes, vec![info.previous_connect_reason as u32]);
7174
7175        let logged_metrics =
7176            test_helper.get_logged_metrics(metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID);
7177        assert_eq!(logged_metrics.len(), 2);
7178        assert_eq!(
7179            logged_metrics[0].event_codes,
7180            vec![metrics::ConnectionScoreAverageMetricDimensionDuration::ShortDuration as u32]
7181        );
7182        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(100));
7183    }
7184
7185    #[fuchsia::test]
7186    fn test_log_disconnect_cobalt_metrics() {
7187        let (mut test_helper, mut test_fut) = setup_test();
7188        test_helper.advance_by(zx::MonotonicDuration::from_hours(3), test_fut.as_mut());
7189        test_helper.send_connected_event(random_bss_description!(Wpa2));
7190        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7191
7192        test_helper.advance_by(zx::MonotonicDuration::from_hours(5), test_fut.as_mut());
7193
7194        let primary_channel = 8;
7195        let channel = Channel::new(primary_channel, Cbw::Cbw20);
7196        let ap_state: client::types::ApState =
7197            random_bss_description!(Wpa2, channel: channel).into();
7198        let info = DisconnectInfo {
7199            connected_duration: zx::MonotonicDuration::from_hours(5),
7200            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
7201                reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
7202                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
7203            }),
7204            ap_state: ap_state.clone(),
7205            ..fake_disconnect_info()
7206        };
7207        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7208            track_subsequent_downtime: true,
7209            info: Some(info),
7210        });
7211        test_helper.drain_cobalt_events(&mut test_fut);
7212
7213        let policy_disconnection_reasons =
7214            test_helper.get_logged_metrics(metrics::POLICY_DISCONNECTION_MIGRATED_METRIC_ID);
7215        assert_eq!(policy_disconnection_reasons.len(), 1);
7216        assert_eq!(policy_disconnection_reasons[0].payload, MetricEventPayload::Count(1));
7217        assert_eq!(
7218            policy_disconnection_reasons[0].event_codes,
7219            vec![client::types::DisconnectReason::DisconnectDetectedFromSme as u32]
7220        );
7221
7222        let disconnect_counts =
7223            test_helper.get_logged_metrics(metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID);
7224        assert_eq!(disconnect_counts.len(), 1);
7225        assert_eq!(disconnect_counts[0].payload, MetricEventPayload::Count(1));
7226
7227        let breakdowns_by_device_uptime = test_helper
7228            .get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_DEVICE_UPTIME_METRIC_ID);
7229        assert_eq!(breakdowns_by_device_uptime.len(), 1);
7230        assert_eq!(breakdowns_by_device_uptime[0].event_codes, vec![
7231            metrics::DisconnectBreakdownByDeviceUptimeMetricDimensionDeviceUptime::LessThan12Hours as u32,
7232        ]);
7233        assert_eq!(breakdowns_by_device_uptime[0].payload, MetricEventPayload::Count(1));
7234
7235        let breakdowns_by_connected_duration = test_helper
7236            .get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_CONNECTED_DURATION_METRIC_ID);
7237        assert_eq!(breakdowns_by_connected_duration.len(), 1);
7238        assert_eq!(breakdowns_by_connected_duration[0].event_codes, vec![
7239            metrics::DisconnectBreakdownByConnectedDurationMetricDimensionConnectedDuration::LessThan6Hours as u32,
7240        ]);
7241        assert_eq!(breakdowns_by_connected_duration[0].payload, MetricEventPayload::Count(1));
7242
7243        let breakdowns_by_reason =
7244            test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID);
7245        assert_eq!(breakdowns_by_reason.len(), 1);
7246        assert_eq!(
7247            breakdowns_by_reason[0].event_codes,
7248            vec![3u32, metrics::ConnectivityWlanMetricDimensionDisconnectSource::Mlme as u32,]
7249        );
7250        assert_eq!(breakdowns_by_reason[0].payload, MetricEventPayload::Count(1));
7251
7252        let breakdowns_by_channel = test_helper
7253            .get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID);
7254        assert_eq!(breakdowns_by_channel.len(), 1);
7255        assert_eq!(breakdowns_by_channel[0].event_codes, vec![channel.primary as u32]);
7256        assert_eq!(breakdowns_by_channel[0].payload, MetricEventPayload::Count(1));
7257
7258        let breakdowns_by_channel_band =
7259            test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID);
7260        assert_eq!(breakdowns_by_channel_band.len(), 1);
7261        assert_eq!(
7262            breakdowns_by_channel_band[0].event_codes,
7263            vec![
7264                metrics::DisconnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz
7265                    as u32
7266            ]
7267        );
7268        assert_eq!(breakdowns_by_channel_band[0].payload, MetricEventPayload::Count(1));
7269
7270        let breakdowns_by_is_multi_bss =
7271            test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID);
7272        assert_eq!(breakdowns_by_is_multi_bss.len(), 1);
7273        assert_eq!(
7274            breakdowns_by_is_multi_bss[0].event_codes,
7275            vec![metrics::DisconnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes as u32]
7276        );
7277        assert_eq!(breakdowns_by_is_multi_bss[0].payload, MetricEventPayload::Count(1));
7278
7279        let breakdowns_by_security_type = test_helper
7280            .get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID);
7281        assert_eq!(breakdowns_by_security_type.len(), 1);
7282        assert_eq!(
7283            breakdowns_by_security_type[0].event_codes,
7284            vec![
7285                metrics::DisconnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal
7286                    as u32
7287            ]
7288        );
7289        assert_eq!(breakdowns_by_security_type[0].payload, MetricEventPayload::Count(1));
7290
7291        // Connected duration should be logged for overall disconnect metric and for non-roam
7292        // metric.
7293        let connected_duration_before_disconnect =
7294            test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID);
7295        assert_eq!(connected_duration_before_disconnect.len(), 1);
7296        assert_eq!(
7297            connected_duration_before_disconnect[0].payload,
7298            MetricEventPayload::IntegerValue(300)
7299        );
7300        let connected_duration_before_non_roam_disconnect = test_helper
7301            .get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_NON_ROAM_DISCONNECT_METRIC_ID);
7302        assert_eq!(connected_duration_before_non_roam_disconnect.len(), 1);
7303        assert_eq!(
7304            connected_duration_before_non_roam_disconnect[0].payload,
7305            MetricEventPayload::IntegerValue(300)
7306        );
7307        let connected_duration_before_roam_attempt = test_helper.get_logged_metrics(
7308            metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
7309        );
7310        assert_eq!(connected_duration_before_roam_attempt.len(), 0);
7311
7312        // Disconnect count should be logged for overall disconnect metric and for non-roam
7313        // metric.
7314        let network_disconnect_counts =
7315            test_helper.get_logged_metrics(metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID);
7316        assert_eq!(network_disconnect_counts.len(), 1);
7317        assert_eq!(network_disconnect_counts[0].payload, MetricEventPayload::Count(1));
7318
7319        let non_roam_disconnect_counts =
7320            test_helper.get_logged_metrics(metrics::NON_ROAM_DISCONNECT_COUNTS_METRIC_ID);
7321        assert_eq!(non_roam_disconnect_counts.len(), 1);
7322        assert_eq!(non_roam_disconnect_counts[0].payload, MetricEventPayload::Count(1));
7323
7324        let roam_disconnect_counts =
7325            test_helper.get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_METRIC_ID);
7326        assert!(roam_disconnect_counts.is_empty());
7327
7328        // Clear events.
7329        test_helper.clear_cobalt_events();
7330
7331        // Advance and get state back to connected.
7332        test_helper.advance_by(zx::MonotonicDuration::from_minutes(1), test_fut.as_mut());
7333        test_helper.send_connected_event(random_bss_description!(Wpa2));
7334        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7335
7336        test_helper.advance_by(zx::MonotonicDuration::from_hours(6), test_fut.as_mut());
7337
7338        // Send a disconnect count with roam cause.
7339        let info = DisconnectInfo {
7340            connected_duration: zx::MonotonicDuration::from_hours(6),
7341            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
7342                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
7343                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
7344            }),
7345            ap_state,
7346            ..fake_disconnect_info()
7347        };
7348        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7349            track_subsequent_downtime: true,
7350            info: Some(info),
7351        });
7352        test_helper.drain_cobalt_events(&mut test_fut);
7353
7354        // Connected duration should be logged for overall disconnect metric, but not for non-roam
7355        // metric.
7356        let connected_duration_before_disconnect =
7357            test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID);
7358        assert_eq!(connected_duration_before_disconnect.len(), 1);
7359        assert_eq!(
7360            connected_duration_before_disconnect[0].payload,
7361            MetricEventPayload::IntegerValue(360)
7362        );
7363
7364        let connected_duration_before_non_roam_disconnect = test_helper
7365            .get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_NON_ROAM_DISCONNECT_METRIC_ID);
7366        assert!(connected_duration_before_non_roam_disconnect.is_empty());
7367
7368        // Connected duration before roam attempt should also not be logged, despite the roam
7369        // disconnect source, because we log that in the roam result event where we can distinguish
7370        // successful roams from failed roams.
7371        let connected_duration_before_roam_attempt = test_helper.get_logged_metrics(
7372            metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
7373        );
7374        assert!(connected_duration_before_roam_attempt.is_empty());
7375
7376        // Disconnect count should be logged for overall disconnect metric, but not for non-roam
7377        // metric.
7378        let network_disconnect_counts =
7379            test_helper.get_logged_metrics(metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID);
7380        assert_eq!(network_disconnect_counts.len(), 1);
7381        assert_eq!(network_disconnect_counts[0].payload, MetricEventPayload::Count(1));
7382
7383        let non_roam_disconnect_counts =
7384            test_helper.get_logged_metrics(metrics::NON_ROAM_DISCONNECT_COUNTS_METRIC_ID);
7385        assert!(non_roam_disconnect_counts.is_empty());
7386
7387        // Roam disconnect count should not be logged, because we log that in the roam result event.
7388        let roam_disconnect_counts =
7389            test_helper.get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_METRIC_ID);
7390        assert!(roam_disconnect_counts.is_empty());
7391    }
7392
7393    #[fuchsia::test]
7394    fn test_log_user_disconnect_cobalt_metrics() {
7395        let (mut test_helper, mut test_fut) = setup_test();
7396        test_helper.advance_by(zx::MonotonicDuration::from_hours(3), test_fut.as_mut());
7397        test_helper.send_connected_event(random_bss_description!(Wpa2));
7398        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7399
7400        const DUR_MIN: i64 = 250;
7401        test_helper.advance_by(zx::MonotonicDuration::from_minutes(DUR_MIN), test_fut.as_mut());
7402
7403        // Send a disconnect event.
7404        let info = DisconnectInfo {
7405            connected_duration: zx::MonotonicDuration::from_minutes(DUR_MIN),
7406            disconnect_source: fidl_sme::DisconnectSource::User(
7407                fidl_sme::UserDisconnectReason::FidlConnectRequest,
7408            ),
7409            ..fake_disconnect_info()
7410        };
7411        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7412            track_subsequent_downtime: true,
7413            info: Some(info),
7414        });
7415        test_helper.drain_cobalt_events(&mut test_fut);
7416
7417        // Check that nothing was logged for roaming disconnects.
7418        let roam_connected_duration = test_helper.get_logged_metrics(
7419            metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
7420        );
7421        assert_eq!(roam_connected_duration.len(), 0);
7422
7423        // Check that a non_roam disconnect was logged
7424        let non_roam_connected_duration = test_helper
7425            .get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_NON_ROAM_DISCONNECT_METRIC_ID);
7426        assert_eq!(non_roam_connected_duration.len(), 1);
7427
7428        let roam_disconnect_counts =
7429            test_helper.get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_METRIC_ID);
7430        assert!(roam_disconnect_counts.is_empty());
7431
7432        let non_roam_disconnect_counts =
7433            test_helper.get_logged_metrics(metrics::NON_ROAM_DISCONNECT_COUNTS_METRIC_ID);
7434        assert!(!non_roam_disconnect_counts.is_empty());
7435
7436        // Check that a connected duration and a count were logged for overall disconnects.
7437        let total_connected_duration =
7438            test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID);
7439        assert_eq!(total_connected_duration.len(), 1);
7440        assert_eq!(total_connected_duration[0].payload, MetricEventPayload::IntegerValue(DUR_MIN));
7441
7442        let total_disconnect_counts =
7443            test_helper.get_logged_metrics(metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID);
7444        assert_eq!(total_disconnect_counts.len(), 1);
7445        assert_eq!(total_disconnect_counts[0].payload, MetricEventPayload::Count(1));
7446    }
7447
7448    #[fuchsia::test]
7449    fn test_log_saved_networks_count() {
7450        let (mut test_helper, mut test_fut) = setup_test();
7451
7452        let event = TelemetryEvent::SavedNetworkCount {
7453            saved_network_count: 4,
7454            config_count_per_saved_network: vec![1, 1],
7455        };
7456        test_helper.telemetry_sender.send(event);
7457        test_helper.drain_cobalt_events(&mut test_fut);
7458
7459        let saved_networks_count =
7460            test_helper.get_logged_metrics(metrics::SAVED_NETWORKS_MIGRATED_METRIC_ID);
7461        assert_eq!(saved_networks_count.len(), 1);
7462        assert_eq!(
7463            saved_networks_count[0].event_codes,
7464            vec![metrics::SavedNetworksMigratedMetricDimensionSavedNetworks::TwoToFour as u32]
7465        );
7466
7467        let config_count = test_helper
7468            .get_logged_metrics(metrics::SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_MIGRATED_METRIC_ID);
7469        assert_eq!(config_count.len(), 2);
7470        assert_eq!(
7471            config_count[0].event_codes,
7472            vec![metrics::SavedConfigurationsForSavedNetworkMigratedMetricDimensionSavedConfigurations::One as u32]
7473        );
7474        assert_eq!(
7475            config_count[1].event_codes,
7476            vec![metrics::SavedConfigurationsForSavedNetworkMigratedMetricDimensionSavedConfigurations::One as u32]
7477        );
7478    }
7479
7480    #[fuchsia::test]
7481    fn test_log_network_selection_scan_interval() {
7482        let (mut test_helper, mut test_fut) = setup_test();
7483
7484        let duration = zx::MonotonicDuration::from_seconds(rand::random_range(0..100));
7485
7486        let event = TelemetryEvent::NetworkSelectionScanInterval { time_since_last_scan: duration };
7487        test_helper.telemetry_sender.send(event);
7488        test_helper.drain_cobalt_events(&mut test_fut);
7489
7490        let last_scan_age = test_helper
7491            .get_logged_metrics(metrics::LAST_SCAN_AGE_WHEN_SCAN_REQUESTED_MIGRATED_METRIC_ID);
7492        assert_eq!(last_scan_age.len(), 1);
7493        assert_eq!(
7494            last_scan_age[0].payload,
7495            fidl_fuchsia_metrics::MetricEventPayload::IntegerValue(duration.into_micros())
7496        );
7497    }
7498
7499    #[fuchsia::test]
7500    fn test_log_connection_selection_scan_results() {
7501        let (mut test_helper, mut test_fut) = setup_test();
7502
7503        let event = TelemetryEvent::ConnectionSelectionScanResults {
7504            saved_network_count: 4,
7505            saved_network_count_found_by_active_scan: 1,
7506            bss_count_per_saved_network: vec![10, 10],
7507        };
7508        test_helper.telemetry_sender.send(event);
7509        test_helper.drain_cobalt_events(&mut test_fut);
7510
7511        let saved_networks_count =
7512            test_helper.get_logged_metrics(metrics::SCAN_RESULTS_RECEIVED_MIGRATED_METRIC_ID);
7513        assert_eq!(saved_networks_count.len(), 1);
7514        assert_eq!(
7515            saved_networks_count[0].event_codes,
7516            vec![
7517                metrics::ScanResultsReceivedMigratedMetricDimensionSavedNetworksCount::TwoToFour
7518                    as u32
7519            ]
7520        );
7521
7522        let active_scanned_network = test_helper.get_logged_metrics(
7523            metrics::SAVED_NETWORK_IN_SCAN_RESULT_WITH_ACTIVE_SCAN_MIGRATED_METRIC_ID,
7524        );
7525        assert_eq!(active_scanned_network.len(), 1);
7526        assert_eq!(
7527            active_scanned_network[0].event_codes,
7528            vec![metrics::SavedNetworkInScanResultWithActiveScanMigratedMetricDimensionActiveScanSsidsObserved::One as u32]
7529        );
7530
7531        let bss_count = test_helper
7532            .get_logged_metrics(metrics::SAVED_NETWORK_IN_SCAN_RESULT_MIGRATED_METRIC_ID);
7533        assert_eq!(bss_count.len(), 2);
7534        assert_eq!(
7535            bss_count[0].event_codes,
7536            vec![
7537                metrics::SavedNetworkInScanResultMigratedMetricDimensionBssCount::FiveToTen as u32
7538            ]
7539        );
7540        assert_eq!(
7541            bss_count[1].event_codes,
7542            vec![
7543                metrics::SavedNetworkInScanResultMigratedMetricDimensionBssCount::FiveToTen as u32
7544            ]
7545        );
7546    }
7547
7548    #[fuchsia::test]
7549    fn test_log_establish_connection_cobalt_metrics() {
7550        let (mut test_helper, mut test_fut) = setup_test();
7551
7552        let primary_channel = 8;
7553        let channel = Channel::new(primary_channel, Cbw::Cbw20);
7554        let ap_state = random_bss_description!(Wpa2,
7555            bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
7556            channel: channel,
7557            rssi_dbm: -50,
7558            snr_db: 25,
7559        )
7560        .into();
7561        let event = TelemetryEvent::ConnectResult {
7562            iface_id: IFACE_ID,
7563            policy_connect_reason: Some(client::types::ConnectReason::FidlConnectRequest),
7564            result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
7565            multiple_bss_candidates: true,
7566            ap_state,
7567            network_is_likely_hidden: true,
7568        };
7569        test_helper.telemetry_sender.send(event);
7570        test_helper.drain_cobalt_events(&mut test_fut);
7571
7572        let policy_connect_reasons =
7573            test_helper.get_logged_metrics(metrics::POLICY_CONNECTION_ATTEMPT_MIGRATED_METRIC_ID);
7574        assert_eq!(policy_connect_reasons.len(), 1);
7575        assert_eq!(
7576            policy_connect_reasons[0].event_codes,
7577            vec![client::types::ConnectReason::FidlConnectRequest as u32]
7578        );
7579        assert_eq!(policy_connect_reasons[0].payload, MetricEventPayload::Count(1));
7580
7581        let breakdowns_by_status_code = test_helper
7582            .get_logged_metrics(metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID);
7583        assert_eq!(breakdowns_by_status_code.len(), 1);
7584        assert_eq!(
7585            breakdowns_by_status_code[0].event_codes,
7586            vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
7587        );
7588        assert_eq!(breakdowns_by_status_code[0].payload, MetricEventPayload::Count(1));
7589
7590        let breakdowns_by_user_wait_time = test_helper
7591            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7592        // TelemetryEvent::StartEstablishConnection is never sent, so connect start time is never
7593        // tracked, hence this metric is not logged.
7594        assert_eq!(breakdowns_by_user_wait_time.len(), 0);
7595
7596        let breakdowns_by_is_multi_bss = test_helper
7597            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID);
7598        assert_eq!(breakdowns_by_is_multi_bss.len(), 1);
7599        assert_eq!(
7600            breakdowns_by_is_multi_bss[0].event_codes,
7601            vec![
7602                metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes
7603                    as u32
7604            ]
7605        );
7606        assert_eq!(breakdowns_by_is_multi_bss[0].payload, MetricEventPayload::Count(1));
7607
7608        let breakdowns_by_security_type = test_helper
7609            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID);
7610        assert_eq!(breakdowns_by_security_type.len(), 1);
7611        assert_eq!(
7612            breakdowns_by_security_type[0].event_codes,
7613            vec![
7614                metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal
7615                    as u32
7616            ]
7617        );
7618        assert_eq!(breakdowns_by_security_type[0].payload, MetricEventPayload::Count(1));
7619
7620        let breakdowns_by_channel = test_helper
7621            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID);
7622        assert_eq!(breakdowns_by_channel.len(), 1);
7623        assert_eq!(breakdowns_by_channel[0].event_codes, vec![primary_channel as u32]);
7624        assert_eq!(breakdowns_by_channel[0].payload, MetricEventPayload::Count(1));
7625
7626        let breakdowns_by_channel_band = test_helper
7627            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID);
7628        assert_eq!(breakdowns_by_channel_band.len(), 1);
7629        assert_eq!(breakdowns_by_channel_band[0].event_codes, vec![
7630            metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz as u32
7631        ]);
7632        assert_eq!(breakdowns_by_channel_band[0].payload, MetricEventPayload::Count(1));
7633
7634        let fidl_connect_count =
7635            test_helper.get_logged_metrics(metrics::POLICY_CONNECTION_ATTEMPTS_METRIC_ID);
7636        assert_eq!(fidl_connect_count.len(), 1);
7637        assert_eq!(fidl_connect_count[0].payload, MetricEventPayload::Count(1));
7638
7639        let network_is_likely_hidden =
7640            test_helper.get_logged_metrics(metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID);
7641        assert_eq!(network_is_likely_hidden.len(), 1);
7642        assert_eq!(network_is_likely_hidden[0].payload, MetricEventPayload::Count(1));
7643    }
7644
7645    #[fuchsia::test]
7646    fn test_log_connect_attempt_breakdown_by_failed_status_code() {
7647        let (mut test_helper, mut test_fut) = setup_test();
7648
7649        let event = TelemetryEvent::ConnectResult {
7650            iface_id: IFACE_ID,
7651            policy_connect_reason: None,
7652            result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedCapabilitiesMismatch),
7653            multiple_bss_candidates: true,
7654            ap_state: random_bss_description!(Wpa2).into(),
7655            network_is_likely_hidden: true,
7656        };
7657        test_helper.telemetry_sender.send(event);
7658        test_helper.drain_cobalt_events(&mut test_fut);
7659
7660        let breakdowns_by_status_code = test_helper
7661            .get_logged_metrics(metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID);
7662        assert_eq!(breakdowns_by_status_code.len(), 1);
7663        assert_eq!(
7664            breakdowns_by_status_code[0].event_codes,
7665            vec![fidl_ieee80211::StatusCode::RefusedCapabilitiesMismatch.into_primitive() as u32]
7666        );
7667    }
7668
7669    #[fuchsia::test]
7670    fn test_log_establish_connection_status_code_cobalt_metrics_normal_device() {
7671        let (mut test_helper, mut test_fut) = setup_test();
7672        for _ in 0..3 {
7673            let event = TelemetryEvent::ConnectResult {
7674                iface_id: IFACE_ID,
7675                policy_connect_reason: Some(
7676                    client::types::ConnectReason::RetryAfterFailedConnectAttempt,
7677                ),
7678                result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
7679                multiple_bss_candidates: true,
7680                ap_state: random_bss_description!(Wpa1).into(),
7681                network_is_likely_hidden: true,
7682            };
7683            test_helper.telemetry_sender.send(event);
7684        }
7685        test_helper.send_connected_event(random_bss_description!(Wpa2));
7686        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
7687
7688        let status_codes = test_helper.get_logged_metrics(
7689            metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7690        );
7691        assert_eq!(status_codes.len(), 2);
7692        assert_eq_cobalt_events(
7693            status_codes,
7694            vec![
7695                MetricEvent {
7696                    metric_id:
7697                        metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7698                    event_codes: vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32],
7699                    payload: MetricEventPayload::Count(1),
7700                },
7701                MetricEvent {
7702                    metric_id:
7703                        metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7704                    event_codes: vec![
7705                        fidl_ieee80211::StatusCode::RefusedReasonUnspecified.into_primitive()
7706                            as u32,
7707                    ],
7708                    payload: MetricEventPayload::Count(3),
7709                },
7710            ],
7711        );
7712    }
7713
7714    #[fuchsia::test]
7715    fn test_log_establish_connection_status_code_cobalt_metrics_bad_device() {
7716        let (mut test_helper, mut test_fut) = setup_test();
7717        for _ in 0..10 {
7718            let event = TelemetryEvent::ConnectResult {
7719                iface_id: IFACE_ID,
7720                policy_connect_reason: Some(
7721                    client::types::ConnectReason::RetryAfterFailedConnectAttempt,
7722                ),
7723                result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
7724                multiple_bss_candidates: true,
7725                ap_state: random_bss_description!(Wpa1).into(),
7726                network_is_likely_hidden: true,
7727            };
7728            test_helper.telemetry_sender.send(event);
7729        }
7730        test_helper.send_connected_event(random_bss_description!(Wpa2));
7731        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
7732
7733        let status_codes = test_helper.get_logged_metrics(
7734            metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7735        );
7736        assert_eq!(status_codes.len(), 2);
7737        assert_eq_cobalt_events(
7738            status_codes,
7739            vec![
7740                MetricEvent {
7741                    metric_id:
7742                        metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7743                    event_codes: vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32],
7744                    payload: MetricEventPayload::Count(1),
7745                },
7746                MetricEvent {
7747                    metric_id:
7748                        metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7749                    event_codes: vec![
7750                        fidl_ieee80211::StatusCode::RefusedReasonUnspecified.into_primitive()
7751                            as u32,
7752                    ],
7753                    payload: MetricEventPayload::Count(10),
7754                },
7755            ],
7756        );
7757    }
7758
7759    #[fuchsia::test]
7760    fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_no_reset() {
7761        let (mut test_helper, mut test_fut) = setup_test();
7762
7763        test_helper
7764            .telemetry_sender
7765            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7766        test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7767        test_helper
7768            .telemetry_sender
7769            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7770        test_helper.advance_by(zx::MonotonicDuration::from_seconds(4), test_fut.as_mut());
7771        test_helper.send_connected_event(random_bss_description!(Wpa2));
7772        test_helper.drain_cobalt_events(&mut test_fut);
7773
7774        let breakdowns_by_user_wait_time = test_helper
7775            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7776        assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7777        assert_eq!(
7778            breakdowns_by_user_wait_time[0].event_codes,
7779            // Both the 2 seconds and 4 seconds since the first StartEstablishConnection
7780            // should be counted.
7781            vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan8Seconds as u32]
7782        );
7783    }
7784
7785    #[fuchsia::test]
7786    fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_with_reset() {
7787        let (mut test_helper, mut test_fut) = setup_test();
7788
7789        test_helper
7790            .telemetry_sender
7791            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7792        test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7793        test_helper
7794            .telemetry_sender
7795            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: true });
7796        test_helper.advance_by(zx::MonotonicDuration::from_seconds(4), test_fut.as_mut());
7797        test_helper.send_connected_event(random_bss_description!(Wpa2));
7798        test_helper.drain_cobalt_events(&mut test_fut);
7799
7800        let breakdowns_by_user_wait_time = test_helper
7801            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7802        assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7803        assert_eq!(
7804            breakdowns_by_user_wait_time[0].event_codes,
7805            // Only the 4 seconds after the last StartEstablishConnection should be counted.
7806            vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan5Seconds as u32]
7807        );
7808    }
7809
7810    #[fuchsia::test]
7811    fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_with_clear() {
7812        let (mut test_helper, mut test_fut) = setup_test();
7813
7814        test_helper
7815            .telemetry_sender
7816            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7817        test_helper.advance_by(zx::MonotonicDuration::from_seconds(10), test_fut.as_mut());
7818        test_helper.telemetry_sender.send(TelemetryEvent::ClearEstablishConnectionStartTime);
7819
7820        test_helper.advance_by(zx::MonotonicDuration::from_seconds(30), test_fut.as_mut());
7821
7822        test_helper
7823            .telemetry_sender
7824            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7825        test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7826        test_helper.send_connected_event(random_bss_description!(Wpa2));
7827        test_helper.drain_cobalt_events(&mut test_fut);
7828
7829        let breakdowns_by_user_wait_time = test_helper
7830            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7831        assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7832        assert_eq!(
7833            breakdowns_by_user_wait_time[0].event_codes,
7834            // Only the 2 seconds after the last StartEstablishConnection should be counted.
7835            vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan3Seconds as u32]
7836        );
7837    }
7838
7839    #[test_case(
7840        (true, random_bss_description!(Wpa2)),
7841        (false, random_bss_description!(Wpa2)),
7842        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
7843        metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes as u32,
7844        metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::No as u32;
7845        "breakdown_by_is_multi_bss"
7846    )]
7847    #[test_case(
7848        (false, random_bss_description!(Wpa1)),
7849        (false, random_bss_description!(Wpa2)),
7850        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
7851        metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa1 as u32,
7852        metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal as u32;
7853        "breakdown_by_security_type"
7854    )]
7855    #[test_case(
7856        (false, random_bss_description!(Wpa2, channel: Channel::new(6, Cbw::Cbw20))),
7857        (false, random_bss_description!(Wpa2, channel: Channel::new(157, Cbw::Cbw40))),
7858        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
7859        6,
7860        157;
7861        "breakdown_by_primary_channel"
7862    )]
7863    #[test_case(
7864        (false, random_bss_description!(Wpa2, channel: Channel::new(6, Cbw::Cbw20))),
7865        (false, random_bss_description!(Wpa2, channel: Channel::new(157, Cbw::Cbw40))),
7866        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
7867        metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz as u32,
7868        metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band5Ghz as u32;
7869        "breakdown_by_channel_band"
7870    )]
7871    #[test_case(
7872        (false, random_bss_description!(Wpa2, rssi_dbm: -79)),
7873        (false, random_bss_description!(Wpa2, rssi_dbm: -40)),
7874        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_RSSI_BUCKET_METRIC_ID,
7875        metrics::ConnectivityWlanMetricDimensionRssiBucket::From79To77 as u32,
7876        metrics::ConnectivityWlanMetricDimensionRssiBucket::From50To35 as u32;
7877        "breakdown_by_rssi_bucket"
7878    )]
7879    #[test_case(
7880        (false, random_bss_description!(Wpa2, snr_db: 11)),
7881        (false, random_bss_description!(Wpa2, snr_db: 35)),
7882        metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SNR_BUCKET_METRIC_ID,
7883        metrics::ConnectivityWlanMetricDimensionSnrBucket::From11To15 as u32,
7884        metrics::ConnectivityWlanMetricDimensionSnrBucket::From26To40 as u32;
7885        "breakdown_by_snr_bucket"
7886    )]
7887    #[fuchsia::test(add_test_attr = false)]
7888    fn test_log_daily_connect_success_rate_breakdown_cobalt_metrics(
7889        first_connect_result_params: (bool, BssDescription),
7890        second_connect_result_params: (bool, BssDescription),
7891        metric_id: u32,
7892        event_code_1: u32,
7893        event_code_2: u32,
7894    ) {
7895        let (mut test_helper, mut test_fut) = setup_test();
7896
7897        for i in 0..3 {
7898            let code = if i == 0 {
7899                fidl_ieee80211::StatusCode::Success
7900            } else {
7901                fidl_ieee80211::StatusCode::RefusedReasonUnspecified
7902            };
7903            let event = TelemetryEvent::ConnectResult {
7904                iface_id: IFACE_ID,
7905                policy_connect_reason: Some(
7906                    client::types::ConnectReason::RetryAfterFailedConnectAttempt,
7907                ),
7908                result: fake_connect_result(code),
7909                multiple_bss_candidates: first_connect_result_params.0,
7910                ap_state: first_connect_result_params.1.clone().into(),
7911                network_is_likely_hidden: true,
7912            };
7913            test_helper.telemetry_sender.send(event);
7914        }
7915        for i in 0..2 {
7916            let code = if i == 0 {
7917                fidl_ieee80211::StatusCode::Success
7918            } else {
7919                fidl_ieee80211::StatusCode::RefusedReasonUnspecified
7920            };
7921            let event = TelemetryEvent::ConnectResult {
7922                iface_id: IFACE_ID,
7923                policy_connect_reason: Some(
7924                    client::types::ConnectReason::RetryAfterFailedConnectAttempt,
7925                ),
7926                result: fake_connect_result(code),
7927                multiple_bss_candidates: second_connect_result_params.0,
7928                ap_state: second_connect_result_params.1.clone().into(),
7929                network_is_likely_hidden: true,
7930            };
7931            test_helper.telemetry_sender.send(event);
7932        }
7933
7934        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
7935
7936        let metrics = test_helper.get_logged_metrics(metric_id);
7937        assert_eq!(metrics.len(), 2);
7938        assert_eq_cobalt_events(
7939            metrics,
7940            vec![
7941                MetricEvent {
7942                    metric_id,
7943                    event_codes: vec![event_code_1],
7944                    payload: MetricEventPayload::IntegerValue(3333), // 1/3 = 33.33%
7945                },
7946                MetricEvent {
7947                    metric_id,
7948                    event_codes: vec![event_code_2],
7949                    payload: MetricEventPayload::IntegerValue(5000), // 1/2 = 50.00%
7950                },
7951            ],
7952        );
7953    }
7954
7955    #[fuchsia::test]
7956    fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_while_connected() {
7957        let (mut test_helper, mut test_fut) = setup_test();
7958        test_helper.send_connected_event(random_bss_description!(Wpa2));
7959        test_helper.drain_cobalt_events(&mut test_fut);
7960        test_helper.cobalt_events.clear();
7961
7962        test_helper
7963            .telemetry_sender
7964            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: true });
7965        test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7966        let info = fake_disconnect_info();
7967        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7968            track_subsequent_downtime: false,
7969            info: Some(info),
7970        });
7971        test_helper.advance_by(zx::MonotonicDuration::from_seconds(4), test_fut.as_mut());
7972        test_helper.send_connected_event(random_bss_description!(Wpa2));
7973        test_helper.drain_cobalt_events(&mut test_fut);
7974
7975        let breakdowns_by_user_wait_time = test_helper
7976            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7977        assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7978        assert_eq!(
7979            breakdowns_by_user_wait_time[0].event_codes,
7980            // Both the 2 seconds and 4 seconds since the first StartEstablishConnection
7981            // should be counted.
7982            vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan8Seconds as u32]
7983        );
7984    }
7985
7986    #[fuchsia::test]
7987    fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_with_clear_while_connected()
7988     {
7989        let (mut test_helper, mut test_fut) = setup_test();
7990        test_helper.send_connected_event(random_bss_description!(Wpa2));
7991        test_helper.drain_cobalt_events(&mut test_fut);
7992        test_helper.cobalt_events.clear();
7993
7994        test_helper
7995            .telemetry_sender
7996            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: true });
7997        test_helper.telemetry_sender.send(TelemetryEvent::ClearEstablishConnectionStartTime);
7998        let info = fake_disconnect_info();
7999        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
8000            track_subsequent_downtime: false,
8001            info: Some(info),
8002        });
8003        test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
8004        test_helper
8005            .telemetry_sender
8006            .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
8007        test_helper.advance_by(zx::MonotonicDuration::from_seconds(4), test_fut.as_mut());
8008        test_helper.send_connected_event(random_bss_description!(Wpa2));
8009        test_helper.drain_cobalt_events(&mut test_fut);
8010
8011        let breakdowns_by_user_wait_time = test_helper
8012            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
8013        assert_eq!(breakdowns_by_user_wait_time.len(), 1);
8014        assert_eq!(
8015            breakdowns_by_user_wait_time[0].event_codes,
8016            // Only the 4 seconds after the last StartEstablishConnection should be counted.
8017            vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan5Seconds as u32]
8018        );
8019    }
8020
8021    #[fuchsia::test]
8022    fn test_log_establish_connection_cobalt_metrics_user_wait_time_logged_for_sme_reconnecting() {
8023        let (mut test_helper, mut test_fut) = setup_test();
8024        test_helper.send_connected_event(random_bss_description!(Wpa2));
8025        test_helper.drain_cobalt_events(&mut test_fut);
8026        test_helper.cobalt_events.clear();
8027
8028        let info = DisconnectInfo { is_sme_reconnecting: true, ..fake_disconnect_info() };
8029        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
8030            track_subsequent_downtime: false,
8031            info: Some(info),
8032        });
8033        test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
8034        test_helper.send_connected_event(random_bss_description!(Wpa2));
8035        test_helper.drain_cobalt_events(&mut test_fut);
8036
8037        let breakdowns_by_user_wait_time = test_helper
8038            .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
8039        assert_eq!(breakdowns_by_user_wait_time.len(), 1);
8040        assert_eq!(
8041            breakdowns_by_user_wait_time[0].event_codes,
8042            vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan3Seconds as u32]
8043        );
8044    }
8045
8046    #[fuchsia::test]
8047    fn test_log_downtime_cobalt_metrics() {
8048        let (mut test_helper, mut test_fut) = setup_test();
8049        test_helper.send_connected_event(random_bss_description!(Wpa2));
8050        test_helper.drain_cobalt_events(&mut test_fut);
8051
8052        let info = DisconnectInfo {
8053            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
8054                reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
8055                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
8056            }),
8057            ..fake_disconnect_info()
8058        };
8059        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
8060            track_subsequent_downtime: true,
8061            info: Some(info),
8062        });
8063        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8064
8065        test_helper.advance_by(zx::MonotonicDuration::from_minutes(42), test_fut.as_mut());
8066        // Indicate that there's no saved neighbor in vicinity
8067        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
8068            network_selection_type: NetworkSelectionType::Undirected,
8069            num_candidates: Ok(0),
8070            selected_count: 0,
8071        });
8072        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8073
8074        test_helper.advance_by(zx::MonotonicDuration::from_minutes(5), test_fut.as_mut());
8075        // Indicate that there's some saved neighbor in vicinity
8076        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
8077            network_selection_type: NetworkSelectionType::Undirected,
8078            num_candidates: Ok(5),
8079            selected_count: 1,
8080        });
8081        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8082
8083        test_helper.advance_by(zx::MonotonicDuration::from_minutes(7), test_fut.as_mut());
8084        // Reconnect
8085        test_helper.send_connected_event(random_bss_description!(Wpa2));
8086        test_helper.drain_cobalt_events(&mut test_fut);
8087
8088        let breakdowns_by_reason = test_helper
8089            .get_logged_metrics(metrics::DOWNTIME_BREAKDOWN_BY_DISCONNECT_REASON_METRIC_ID);
8090        assert_eq!(breakdowns_by_reason.len(), 1);
8091        assert_eq!(
8092            breakdowns_by_reason[0].event_codes,
8093            vec![3u32, metrics::ConnectivityWlanMetricDimensionDisconnectSource::Mlme as u32,]
8094        );
8095        assert_eq!(
8096            breakdowns_by_reason[0].payload,
8097            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(49).into_micros())
8098        );
8099    }
8100
8101    #[fuchsia::test]
8102    fn test_log_reconnect_cobalt_metrics() {
8103        let (mut test_helper, mut test_fut) = setup_test();
8104        test_helper.send_connected_event(random_bss_description!(Wpa2));
8105        test_helper.drain_cobalt_events(&mut test_fut);
8106
8107        // Send disconnect with non-roam cause.
8108        let info = DisconnectInfo {
8109            disconnect_source: fidl_sme::DisconnectSource::User(
8110                fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch,
8111            ),
8112            ..fake_disconnect_info()
8113        };
8114        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
8115            track_subsequent_downtime: true,
8116            info: Some(info),
8117        });
8118        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8119
8120        test_helper.advance_by(zx::MonotonicDuration::from_seconds(3), test_fut.as_mut());
8121        // Reconnect.
8122        test_helper.send_connected_event(random_bss_description!(Wpa2));
8123        test_helper.drain_cobalt_events(&mut test_fut);
8124
8125        // Verify the reconnect duration was logged for a non-roam disconnect only.
8126        let metrics =
8127            test_helper.get_logged_metrics(metrics::NON_ROAM_RECONNECT_DURATION_METRIC_ID);
8128        assert_eq!(metrics.len(), 1);
8129        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(3_000_000));
8130        assert!(
8131            test_helper
8132                .get_logged_metrics(metrics::POLICY_ROAM_RECONNECT_DURATION_METRIC_ID)
8133                .is_empty()
8134        );
8135
8136        // Send a disconnect with a roaming cause.
8137        test_helper.clear_cobalt_events();
8138        let info = DisconnectInfo {
8139            disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
8140                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
8141                mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
8142            }),
8143            ..fake_disconnect_info()
8144        };
8145        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
8146            track_subsequent_downtime: true,
8147            info: Some(info),
8148        });
8149        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8150        test_helper.advance_by(zx::MonotonicDuration::from_seconds(1), test_fut.as_mut());
8151        // Reconnect.
8152        test_helper.send_connected_event(random_bss_description!(Wpa2));
8153        test_helper.drain_cobalt_events(&mut test_fut);
8154
8155        // Verify the reconnect duration was NOT logged for a non-roam reconnect, since the cause
8156        // was a roam cause.
8157        assert!(
8158            test_helper
8159                .get_logged_metrics(metrics::NON_ROAM_RECONNECT_DURATION_METRIC_ID)
8160                .is_empty()
8161        );
8162        // Verify the reconnect duration is also NOT logged for a roam reconnect, despite the roam
8163        // cause, as roam reconnect durations are logged in the roam result event where we can
8164        // distinguish successful roams from failures.
8165        assert!(
8166            test_helper
8167                .get_logged_metrics(metrics::POLICY_ROAM_RECONNECT_DURATION_METRIC_ID)
8168                .is_empty()
8169        );
8170    }
8171
8172    #[fuchsia::test]
8173    fn test_log_device_connected_cobalt_metrics() {
8174        let (mut test_helper, mut test_fut) = setup_test();
8175
8176        let wmm_info = vec![0x80]; // U-APSD enabled
8177        #[rustfmt::skip]
8178        let rm_enabled_capabilities = vec![
8179            0x03, // link measurement and neighbor report enabled
8180            0x00, 0x00, 0x00, 0x00,
8181        ];
8182        #[rustfmt::skip]
8183        let ext_capabilities = vec![
8184            0x04, 0x00,
8185            0x08, // BSS transition supported
8186            0x00, 0x00, 0x00, 0x00, 0x40
8187        ];
8188        let bss_description = random_bss_description!(Wpa2,
8189            channel: Channel::new(157, Cbw::Cbw40),
8190            ies_overrides: IesOverrides::new()
8191                .remove(IeType::WMM_PARAM)
8192                .set(IeType::WMM_INFO, wmm_info)
8193                .set(IeType::RM_ENABLED_CAPABILITIES, rm_enabled_capabilities)
8194                .set(IeType::MOBILITY_DOMAIN, vec![0x00; 3])
8195                .set(IeType::EXT_CAPABILITIES, ext_capabilities),
8196            bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
8197        );
8198        test_helper.send_connected_event(bss_description);
8199        test_helper.drain_cobalt_events(&mut test_fut);
8200
8201        let num_devices_connected =
8202            test_helper.get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID);
8203        assert_eq!(num_devices_connected.len(), 1);
8204        assert_eq!(num_devices_connected[0].payload, MetricEventPayload::Count(1));
8205
8206        let connected_security_type =
8207            test_helper.get_logged_metrics(metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID);
8208        assert_eq!(connected_security_type.len(), 1);
8209        assert_eq!(
8210            connected_security_type[0].event_codes,
8211            vec![
8212                metrics::ConnectedNetworkSecurityTypeMetricDimensionSecurityType::Wpa2Personal
8213                    as u32
8214            ]
8215        );
8216        assert_eq!(connected_security_type[0].payload, MetricEventPayload::Count(1));
8217
8218        let connected_apsd = test_helper
8219            .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID);
8220        assert_eq!(connected_apsd.len(), 1);
8221        assert_eq!(connected_apsd[0].payload, MetricEventPayload::Count(1));
8222
8223        let connected_link_measurement = test_helper.get_logged_metrics(
8224            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
8225        );
8226        assert_eq!(connected_link_measurement.len(), 1);
8227        assert_eq!(connected_link_measurement[0].payload, MetricEventPayload::Count(1));
8228
8229        let connected_neighbor_report = test_helper.get_logged_metrics(
8230            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
8231        );
8232        assert_eq!(connected_neighbor_report.len(), 1);
8233        assert_eq!(connected_neighbor_report[0].payload, MetricEventPayload::Count(1));
8234
8235        let connected_ft = test_helper
8236            .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID);
8237        assert_eq!(connected_ft.len(), 1);
8238        assert_eq!(connected_ft[0].payload, MetricEventPayload::Count(1));
8239
8240        let connected_bss_transition_mgmt = test_helper.get_logged_metrics(
8241            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
8242        );
8243        assert_eq!(connected_bss_transition_mgmt.len(), 1);
8244        assert_eq!(connected_bss_transition_mgmt[0].payload, MetricEventPayload::Count(1));
8245
8246        let breakdown_by_is_multi_bss = test_helper.get_logged_metrics(
8247            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
8248        );
8249        assert_eq!(breakdown_by_is_multi_bss.len(), 1);
8250        assert_eq!(
8251            breakdown_by_is_multi_bss[0].event_codes,
8252            vec![
8253                metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes
8254                    as u32
8255            ]
8256        );
8257        assert_eq!(breakdown_by_is_multi_bss[0].payload, MetricEventPayload::Count(1));
8258
8259        let breakdown_by_primary_channel = test_helper.get_logged_metrics(
8260            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
8261        );
8262        assert_eq!(breakdown_by_primary_channel.len(), 1);
8263        assert_eq!(breakdown_by_primary_channel[0].event_codes, vec![157]);
8264        assert_eq!(breakdown_by_primary_channel[0].payload, MetricEventPayload::Count(1));
8265
8266        let breakdown_by_channel_band = test_helper.get_logged_metrics(
8267            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
8268        );
8269        assert_eq!(breakdown_by_channel_band.len(), 1);
8270        assert_eq!(
8271            breakdown_by_channel_band[0].event_codes,
8272            vec![
8273                metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band5Ghz
8274                    as u32
8275            ]
8276        );
8277        assert_eq!(breakdown_by_channel_band[0].payload, MetricEventPayload::Count(1));
8278
8279        let ap_oui_connected =
8280            test_helper.get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID);
8281        assert_eq!(ap_oui_connected.len(), 1);
8282        assert_eq!(
8283            ap_oui_connected[0].payload,
8284            MetricEventPayload::StringValue("00F620".to_string())
8285        );
8286
8287        let network_is_likely_hidden =
8288            test_helper.get_logged_metrics(metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID);
8289        assert_eq!(network_is_likely_hidden.len(), 1);
8290        assert_eq!(network_is_likely_hidden[0].payload, MetricEventPayload::Count(1));
8291    }
8292
8293    #[fuchsia::test]
8294    fn test_log_device_connected_cobalt_metrics_ap_features_not_supported() {
8295        let (mut test_helper, mut test_fut) = setup_test();
8296
8297        let bss_description = random_bss_description!(Wpa2,
8298            ies_overrides: IesOverrides::new()
8299                .remove(IeType::WMM_PARAM)
8300                .remove(IeType::WMM_INFO)
8301                .remove(IeType::RM_ENABLED_CAPABILITIES)
8302                .remove(IeType::MOBILITY_DOMAIN)
8303                .remove(IeType::EXT_CAPABILITIES)
8304        );
8305        test_helper.send_connected_event(bss_description);
8306        test_helper.drain_cobalt_events(&mut test_fut);
8307
8308        let connected_apsd = test_helper
8309            .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID);
8310        assert_eq!(connected_apsd.len(), 0);
8311
8312        let connected_link_measurement = test_helper.get_logged_metrics(
8313            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
8314        );
8315        assert_eq!(connected_link_measurement.len(), 0);
8316
8317        let connected_neighbor_report = test_helper.get_logged_metrics(
8318            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
8319        );
8320        assert_eq!(connected_neighbor_report.len(), 0);
8321
8322        let connected_ft = test_helper
8323            .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID);
8324        assert_eq!(connected_ft.len(), 0);
8325
8326        let connected_bss_transition_mgmt = test_helper.get_logged_metrics(
8327            metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
8328        );
8329        assert_eq!(connected_bss_transition_mgmt.len(), 0);
8330    }
8331
8332    #[test_case(metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID, None; "connect_to_likely_hidden_network")]
8333    #[test_case(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID, None; "number_of_connected_devices")]
8334    #[test_case(metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID, None; "breakdown_by_security_type")]
8335    #[test_case(metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID, None; "breakdown_by_is_multi_bss")]
8336    #[test_case(metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID, None; "breakdown_by_primary_channel")]
8337    #[test_case(metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID, None; "breakdown_by_channel_band")]
8338    #[test_case(metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID,
8339        Some(vec![
8340            MetricEvent {
8341                metric_id: metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID,
8342                event_codes: vec![],
8343                payload: MetricEventPayload::StringValue("00F620".to_string()),
8344            },
8345        ]); "number_of_devices_connected_to_specific_oui")]
8346    #[fuchsia::test(add_test_attr = false)]
8347    fn test_log_device_connected_cobalt_metrics_on_disconnect_and_periodically(
8348        metric_id: u32,
8349        payload: Option<Vec<MetricEvent>>,
8350    ) {
8351        let (mut test_helper, mut test_fut) = setup_test();
8352
8353        let bss_description = random_bss_description!(Wpa2,
8354            bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
8355        );
8356        test_helper.send_connected_event(bss_description);
8357        test_helper.drain_cobalt_events(&mut test_fut);
8358        test_helper.cobalt_events.clear();
8359
8360        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
8361
8362        // Verify that after 24 hours has passed, metric is logged at least once because
8363        // device is still connected
8364        let metrics = test_helper.get_logged_metrics(metric_id);
8365        assert!(!metrics.is_empty());
8366
8367        if let Some(payload) = payload {
8368            assert_eq_cobalt_events(metrics, payload)
8369        }
8370
8371        test_helper.cobalt_events.clear();
8372
8373        let info = fake_disconnect_info();
8374        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
8375            track_subsequent_downtime: false,
8376            info: Some(info),
8377        });
8378        test_helper.drain_cobalt_events(&mut test_fut);
8379
8380        // Verify that on disconnect, device connected metric is also logged.
8381        let metrics = test_helper.get_logged_metrics(metric_id);
8382        assert_eq!(metrics.len(), 1);
8383    }
8384
8385    #[fuchsia::test]
8386    fn test_log_device_connected_cobalt_metrics_on_channel_switched() {
8387        let (mut test_helper, mut test_fut) = setup_test();
8388        let bss_description = random_bss_description!(Wpa2,
8389            channel: Channel::new(4, Cbw::Cbw20),
8390        );
8391        test_helper.send_connected_event(bss_description);
8392        test_helper.drain_cobalt_events(&mut test_fut);
8393
8394        let breakdown_by_primary_channel = test_helper.get_logged_metrics(
8395            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
8396        );
8397        assert_eq!(breakdown_by_primary_channel.len(), 1);
8398        assert_eq!(breakdown_by_primary_channel[0].event_codes, vec![4]);
8399        assert_eq!(breakdown_by_primary_channel[0].payload, MetricEventPayload::Count(1));
8400
8401        let breakdown_by_channel_band = test_helper.get_logged_metrics(
8402            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
8403        );
8404        assert_eq!(breakdown_by_channel_band.len(), 1);
8405        assert_eq!(
8406            breakdown_by_channel_band[0].event_codes,
8407            vec![
8408                metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz
8409                    as u32
8410            ]
8411        );
8412        assert_eq!(breakdown_by_channel_band[0].payload, MetricEventPayload::Count(1));
8413
8414        // Clear out existing Cobalt metrics
8415        test_helper.cobalt_events.clear();
8416
8417        test_helper.telemetry_sender.send(TelemetryEvent::OnChannelSwitched {
8418            info: fidl_internal::ChannelSwitchInfo { new_channel: 157 },
8419        });
8420        test_helper.drain_cobalt_events(&mut test_fut);
8421
8422        // On channel switched, device connected metrics for the new channel and channel band
8423        // are logged.
8424        let breakdown_by_primary_channel = test_helper.get_logged_metrics(
8425            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
8426        );
8427        assert_eq!(breakdown_by_primary_channel.len(), 1);
8428        assert_eq!(breakdown_by_primary_channel[0].event_codes, vec![157]);
8429        assert_eq!(breakdown_by_primary_channel[0].payload, MetricEventPayload::Count(1));
8430
8431        let breakdown_by_channel_band = test_helper.get_logged_metrics(
8432            metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
8433        );
8434        assert_eq!(breakdown_by_channel_band.len(), 1);
8435        assert_eq!(
8436            breakdown_by_channel_band[0].event_codes,
8437            vec![
8438                metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band5Ghz
8439                    as u32
8440            ]
8441        );
8442        assert_eq!(breakdown_by_channel_band[0].payload, MetricEventPayload::Count(1));
8443    }
8444
8445    #[fuchsia::test]
8446    fn test_active_scan_requested_metric() {
8447        let (mut test_helper, mut test_fut) = setup_test();
8448
8449        test_helper
8450            .telemetry_sender
8451            .send(TelemetryEvent::ActiveScanRequested { num_ssids_requested: 4 });
8452
8453        test_helper.drain_cobalt_events(&mut test_fut);
8454        let metrics = test_helper.get_logged_metrics(
8455            metrics::ACTIVE_SCAN_REQUESTED_FOR_NETWORK_SELECTION_MIGRATED_METRIC_ID,
8456        );
8457        assert_eq!(metrics.len(), 1);
8458        assert_eq!(metrics[0].event_codes, vec![metrics::ActiveScanRequestedForNetworkSelectionMigratedMetricDimensionActiveScanSsidsRequested::TwoToFour as u32]);
8459        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8460    }
8461
8462    #[fuchsia::test]
8463    fn test_log_device_performed_roaming_scan() {
8464        let (mut test_helper, mut test_fut) = setup_test();
8465
8466        // Send a roaming scan event
8467        test_helper.telemetry_sender.send(TelemetryEvent::PolicyRoamScan {
8468            reasons: vec![RoamReason::RssiBelowThreshold, RoamReason::SnrBelowThreshold],
8469        });
8470        test_helper.drain_cobalt_events(&mut test_fut);
8471
8472        // Check that the event was logged to cobalt.
8473        let metrics = test_helper.get_logged_metrics(metrics::POLICY_ROAM_SCAN_COUNT_METRIC_ID);
8474        assert_eq!(metrics.len(), 1);
8475        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8476
8477        // Check that an event was logged for each roam reason.
8478        let metrics = test_helper
8479            .get_logged_metrics(metrics::POLICY_ROAM_SCAN_COUNT_BY_ROAM_REASON_METRIC_ID);
8480        assert_eq!(metrics.len(), 2);
8481        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8482        assert_eq!(
8483            metrics[0].event_codes,
8484            vec![convert::convert_roam_reason_dimension(RoamReason::RssiBelowThreshold) as u32]
8485        );
8486        assert_eq!(metrics[1].payload, MetricEventPayload::Count(1));
8487        assert_eq!(
8488            metrics[1].event_codes,
8489            vec![convert::convert_roam_reason_dimension(RoamReason::SnrBelowThreshold) as u32]
8490        );
8491    }
8492
8493    #[fuchsia::test]
8494    fn test_log_policy_roam_attempt() {
8495        let (mut test_helper, mut test_fut) = setup_test();
8496
8497        // Send a roaming scan event
8498        let candidate = generate_random_scanned_candidate();
8499        test_helper.telemetry_sender.send(TelemetryEvent::PolicyRoamAttempt {
8500            request: PolicyRoamRequest {
8501                candidate,
8502                reasons: vec![RoamReason::RssiBelowThreshold, RoamReason::SnrBelowThreshold],
8503            },
8504            connected_duration: zx::Duration::from_hours(1),
8505        });
8506        test_helper.drain_cobalt_events(&mut test_fut);
8507
8508        let metrics = test_helper.get_logged_metrics(metrics::POLICY_ROAM_ATTEMPT_COUNT_METRIC_ID);
8509        assert_eq!(metrics.len(), 1);
8510        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8511
8512        // Check that an event was logged for each roam reason.
8513        let metrics = test_helper
8514            .get_logged_metrics(metrics::POLICY_ROAM_ATTEMPT_COUNT_BY_ROAM_REASON_METRIC_ID);
8515        assert_eq!(metrics.len(), 2);
8516        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8517        assert_eq!(
8518            metrics[0].event_codes,
8519            vec![convert::convert_roam_reason_dimension(RoamReason::RssiBelowThreshold) as u32]
8520        );
8521        assert_eq!(metrics[1].payload, MetricEventPayload::Count(1));
8522        assert_eq!(
8523            metrics[1].event_codes,
8524            vec![convert::convert_roam_reason_dimension(RoamReason::SnrBelowThreshold) as u32]
8525        );
8526
8527        // Check that a metric was logged for the connedted duration before roaming
8528        let metrics = test_helper.get_logged_metrics(
8529            metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
8530        );
8531        assert_eq!(metrics.len(), 2);
8532        assert_eq!(metrics.len(), 2);
8533        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(60));
8534        assert_eq!(
8535            metrics[0].event_codes,
8536            vec![convert::convert_roam_reason_dimension(RoamReason::RssiBelowThreshold) as u32]
8537        );
8538        assert_eq!(metrics[1].payload, MetricEventPayload::IntegerValue(60));
8539        assert_eq!(
8540            metrics[1].event_codes,
8541            vec![convert::convert_roam_reason_dimension(RoamReason::SnrBelowThreshold) as u32]
8542        );
8543    }
8544
8545    /// Helper function for policy roam success rate tests
8546    fn log_policy_roam_attempt_and_result(
8547        test_helper: &mut TestHelper,
8548        is_success: bool,
8549        reasons: Vec<RoamReason>,
8550    ) {
8551        let status_code = if is_success {
8552            fidl_ieee80211::StatusCode::Success
8553        } else {
8554            fidl_ieee80211::StatusCode::RefusedReasonUnspecified
8555        };
8556
8557        // Log roam attempt
8558        let request = PolicyRoamRequest { candidate: generate_random_scanned_candidate(), reasons };
8559        let event = TelemetryEvent::PolicyRoamAttempt {
8560            request: request.clone(),
8561            connected_duration: zx::MonotonicDuration::from_hours(1),
8562        };
8563        test_helper.telemetry_sender.send(event);
8564
8565        // Log roam result with status code
8566        let result = fidl_sme::RoamResult {
8567            bssid: [1, 1, 1, 1, 1, 1],
8568            status_code,
8569            original_association_maintained: false,
8570            bss_description: Some(Box::new(random_fidl_bss_description!())),
8571            disconnect_info: None,
8572            is_credential_rejected: false,
8573        };
8574
8575        let event = TelemetryEvent::PolicyInitiatedRoamResult {
8576            iface_id: IFACE_ID,
8577            result,
8578            updated_ap_state: random_bss_description!().into(),
8579            original_ap_state: Box::new(random_bss_description!().into()),
8580            request: Box::new(request.clone()),
8581            request_time: fasync::MonotonicInstant::now(),
8582            result_time: fasync::MonotonicInstant::now(),
8583        };
8584        test_helper.telemetry_sender.send(event);
8585    }
8586
8587    #[fuchsia::test]
8588    fn test_log_policy_roam_success_rate_cobalt_metrics() {
8589        let (mut test_helper, mut test_fut) = setup_test();
8590        test_helper.send_connected_event(random_bss_description!(Wpa1));
8591
8592        // Log two roam successes
8593        log_policy_roam_attempt_and_result(
8594            &mut test_helper,
8595            true,
8596            vec![RoamReason::RssiBelowThreshold],
8597        );
8598        log_policy_roam_attempt_and_result(
8599            &mut test_helper,
8600            true,
8601            vec![RoamReason::RssiBelowThreshold],
8602        );
8603
8604        // Log one roam failure
8605        log_policy_roam_attempt_and_result(
8606            &mut test_helper,
8607            false,
8608            vec![RoamReason::RssiBelowThreshold],
8609        );
8610
8611        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
8612
8613        let metrics = test_helper.get_logged_metrics(metrics::POLICY_ROAM_SUCCESS_RATE_METRIC_ID);
8614        assert_eq!(metrics.len(), 1);
8615        assert_eq_cobalt_events(
8616            metrics,
8617            vec![MetricEvent {
8618                metric_id: metrics::POLICY_ROAM_SUCCESS_RATE_METRIC_ID,
8619                event_codes: vec![],
8620                payload: MetricEventPayload::IntegerValue(6666), // 66.66% success rate
8621            }],
8622        );
8623    }
8624
8625    #[fuchsia::test]
8626    fn test_log_policy_roam_success_rate_by_roam_reason_cobalt_metrics() {
8627        let (mut test_helper, mut test_fut) = setup_test();
8628        test_helper.send_connected_event(random_bss_description!(Wpa1));
8629
8630        // Log two roam successes with different reason event code vectors
8631        log_policy_roam_attempt_and_result(
8632            &mut test_helper,
8633            true,
8634            vec![RoamReason::RssiBelowThreshold],
8635        );
8636        log_policy_roam_attempt_and_result(
8637            &mut test_helper,
8638            true,
8639            vec![RoamReason::RssiBelowThreshold, RoamReason::SnrBelowThreshold],
8640        );
8641
8642        // Log one roam failure
8643        log_policy_roam_attempt_and_result(
8644            &mut test_helper,
8645            false,
8646            vec![RoamReason::RssiBelowThreshold],
8647        );
8648
8649        test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
8650
8651        let metrics = test_helper
8652            .get_logged_metrics(metrics::POLICY_ROAM_SUCCESS_RATE_BY_ROAM_REASON_METRIC_ID);
8653        assert_eq!(metrics.len(), 2);
8654        assert_eq_cobalt_events(
8655            metrics,
8656            vec![
8657                MetricEvent {
8658                    metric_id: metrics::POLICY_ROAM_SUCCESS_RATE_BY_ROAM_REASON_METRIC_ID,
8659                    event_codes: vec![convert::convert_roam_reason_dimension(
8660                        RoamReason::RssiBelowThreshold,
8661                    ) as u32],
8662                    payload: MetricEventPayload::IntegerValue(6666), // 66.66% success for RssiBelowThreshold
8663                },
8664                MetricEvent {
8665                    metric_id: metrics::POLICY_ROAM_SUCCESS_RATE_BY_ROAM_REASON_METRIC_ID,
8666                    event_codes: vec![convert::convert_roam_reason_dimension(
8667                        RoamReason::SnrBelowThreshold,
8668                    ) as u32],
8669                    payload: MetricEventPayload::IntegerValue(10000), // 100% success for SnrBelowThreshold
8670                },
8671            ],
8672        );
8673    }
8674
8675    #[fuchsia::test]
8676    fn test_connection_enabled_duration_metric() {
8677        let (mut test_helper, mut test_fut) = setup_test();
8678
8679        test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8680        assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8681        test_helper.advance_by(zx::MonotonicDuration::from_seconds(10), test_fut.as_mut());
8682        test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8683
8684        test_helper.drain_cobalt_events(&mut test_fut);
8685        let metrics = test_helper
8686            .get_logged_metrics(metrics::CLIENT_CONNECTIONS_ENABLED_DURATION_MIGRATED_METRIC_ID);
8687        assert_eq!(metrics.len(), 1);
8688        assert_eq!(
8689            metrics[0].payload,
8690            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_seconds(10).into_micros())
8691        );
8692    }
8693
8694    #[fuchsia::test]
8695    fn test_restart_metric_start_client_connections_request_sent_first() {
8696        let (mut test_helper, mut test_fut) = setup_test();
8697
8698        // Send a start client connections event and then a stop and start corresponding to a
8699        // restart. The first start client connections should not count for the metric.
8700        test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8701        test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
8702        test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8703        test_helper.advance_by(zx::MonotonicDuration::from_seconds(1), test_fut.as_mut());
8704        test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8705
8706        // Check that exactly 1 restart client connections event was logged to cobalt.
8707        test_helper.drain_cobalt_events(&mut test_fut);
8708        let metrics =
8709            test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
8710        assert_eq!(metrics.len(), 1);
8711        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8712    }
8713
8714    #[fuchsia::test]
8715    fn test_restart_metric_stop_client_connections_request_sent_first() {
8716        let (mut test_helper, mut test_fut) = setup_test();
8717
8718        // Send stop and start events corresponding to restarting client connections.
8719        test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8720        test_helper.advance_by(zx::MonotonicDuration::from_seconds(3), test_fut.as_mut());
8721        test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8722        // Check that 1 restart client connection event has been logged to cobalt.
8723        test_helper.drain_cobalt_events(&mut test_fut);
8724        let metrics =
8725            test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
8726        assert_eq!(metrics.len(), 1);
8727        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8728
8729        // Stop and start client connections quickly again.
8730        test_helper.advance_by(zx::MonotonicDuration::from_seconds(20), test_fut.as_mut());
8731        test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8732        test_helper.advance_by(zx::MonotonicDuration::from_seconds(1), test_fut.as_mut());
8733        test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8734        // Check that 1 more event has been logged.
8735        test_helper.drain_cobalt_events(&mut test_fut);
8736        let metrics =
8737            test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
8738        assert_eq!(metrics.len(), 2);
8739        assert_eq!(metrics[1].payload, MetricEventPayload::Count(1));
8740    }
8741
8742    #[fuchsia::test]
8743    fn test_restart_metric_stop_client_connections_request_long_time_not_counted() {
8744        let (mut test_helper, mut test_fut) = setup_test();
8745
8746        // Send a stop and start with some time in between, then a quick stop and start.
8747        test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8748        test_helper.advance_by(zx::MonotonicDuration::from_seconds(30), test_fut.as_mut());
8749        test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8750        test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
8751        // Check that a restart was not logged since some time passed between requests.
8752        test_helper.drain_cobalt_events(&mut test_fut);
8753        let metrics =
8754            test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
8755        assert!(metrics.is_empty());
8756
8757        // Send another stop and start that do correspond to a restart.
8758        test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8759        test_helper.advance_by(zx::MonotonicDuration::from_seconds(1), test_fut.as_mut());
8760        test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8761        // Check that exactly 1 restart client connections event was logged to cobalt.
8762        test_helper.drain_cobalt_events(&mut test_fut);
8763        let metrics =
8764            test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
8765        assert_eq!(metrics.len(), 1);
8766        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8767    }
8768
8769    #[fuchsia::test]
8770    fn test_restart_metric_extra_stop_client_connections_ignored() {
8771        let (mut test_helper, mut test_fut) = setup_test();
8772
8773        // Stop client connections well before starting it again.
8774        test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8775        test_helper.advance_by(zx::MonotonicDuration::from_seconds(10), test_fut.as_mut());
8776
8777        // Send another stop client connections shortly before a start request. The second request
8778        // should not cause a metric to be logged, since connections were already off.
8779        test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8780        test_helper.advance_by(zx::MonotonicDuration::from_seconds(1), test_fut.as_mut());
8781        test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8782
8783        test_helper.drain_cobalt_events(&mut test_fut);
8784        let metrics =
8785            test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
8786        assert!(metrics.is_empty());
8787    }
8788
8789    #[fuchsia::test]
8790    fn test_stop_ap_metric() {
8791        let (mut test_helper, mut test_fut) = setup_test();
8792
8793        test_helper.telemetry_sender.send(TelemetryEvent::StopAp {
8794            enabled_duration: zx::MonotonicDuration::from_seconds(50),
8795        });
8796
8797        test_helper.drain_cobalt_events(&mut test_fut);
8798        let metrics = test_helper
8799            .get_logged_metrics(metrics::ACCESS_POINT_ENABLED_DURATION_MIGRATED_METRIC_ID);
8800        assert_eq!(metrics.len(), 1);
8801        assert_eq!(
8802            metrics[0].payload,
8803            MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_seconds(50).into_micros())
8804        );
8805    }
8806
8807    #[derive(PartialEq)]
8808    enum CreateMetricsLoggerFailureMode {
8809        None,
8810        FactoryRequest,
8811        ApiFailure,
8812    }
8813
8814    #[test_case(CreateMetricsLoggerFailureMode::None)]
8815    #[test_case(CreateMetricsLoggerFailureMode::FactoryRequest)]
8816    #[test_case(CreateMetricsLoggerFailureMode::ApiFailure)]
8817    #[fuchsia::test]
8818    fn test_create_metrics_logger(failure_mode: CreateMetricsLoggerFailureMode) {
8819        let mut exec = fasync::TestExecutor::new();
8820        let (factory_proxy, mut factory_stream) = fidl::endpoints::create_proxy_and_stream::<
8821            fidl_fuchsia_metrics::MetricEventLoggerFactoryMarker,
8822        >();
8823
8824        let fut = create_metrics_logger(&factory_proxy);
8825        let mut fut = pin!(fut);
8826
8827        // First, test the case where the factory service cannot be reached and expect an error.
8828        if failure_mode == CreateMetricsLoggerFailureMode::FactoryRequest {
8829            drop(factory_stream);
8830            assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(Err(_)));
8831            return;
8832        }
8833
8834        // If the test case is intended to allow the factory service to be contacted, run the
8835        // request future until stalled.
8836        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
8837
8838        let request = exec.run_until_stalled(&mut factory_stream.next());
8839        assert_matches!(
8840            request,
8841            Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerFactoryRequest::CreateMetricEventLogger {
8842                project_spec: fidl_fuchsia_metrics::ProjectSpec {
8843                    customer_id: None,
8844                    project_id: Some(metrics::PROJECT_ID),
8845                    ..
8846                },
8847                responder,
8848                ..
8849            }))) => {
8850                match failure_mode {
8851                    CreateMetricsLoggerFailureMode::FactoryRequest => panic!("The factory request failure should have been handled already."),
8852                    CreateMetricsLoggerFailureMode::None => responder.send(Ok(())).expect("failed to send response"),
8853                    CreateMetricsLoggerFailureMode::ApiFailure => responder.send(Err(fidl_fuchsia_metrics::Error::InvalidArguments)).expect("failed to send response"),
8854                }
8855            }
8856        );
8857
8858        // The future should run to completion and the output will vary depending on the specified
8859        // failure mode.
8860        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(result) => {
8861            match failure_mode {
8862                CreateMetricsLoggerFailureMode::FactoryRequest => panic!("The factory request failure should have been handled already."),
8863                CreateMetricsLoggerFailureMode::None => assert_matches!(result, Ok(_)),
8864                CreateMetricsLoggerFailureMode::ApiFailure => assert_matches!(result, Err(_))
8865            }
8866        });
8867    }
8868
8869    #[fuchsia::test]
8870    fn test_log_iface_creation_failure() {
8871        let (mut test_helper, mut test_fut) = setup_test();
8872
8873        // Send a notification that interface creation has failed.
8874        test_helper.telemetry_sender.send(TelemetryEvent::IfaceCreationResult(Err(())));
8875
8876        // Run the telemetry loop until it stalls.
8877        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8878
8879        // Expect that Cobalt has been notified of the interface creation failure.
8880        test_helper.drain_cobalt_events(&mut test_fut);
8881        let logged_metrics =
8882            test_helper.get_logged_metrics(metrics::INTERFACE_CREATION_FAILURE_METRIC_ID);
8883        assert_eq!(logged_metrics.len(), 1);
8884    }
8885
8886    #[fuchsia::test]
8887    fn test_log_iface_destruction_failure() {
8888        let (mut test_helper, mut test_fut) = setup_test();
8889
8890        // Send a notification that interface creation has failed.
8891        test_helper.telemetry_sender.send(TelemetryEvent::IfaceDestructionResult(Err(())));
8892
8893        // Run the telemetry loop until it stalls.
8894        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8895
8896        // Expect that Cobalt has been notified of the interface creation failure.
8897        test_helper.drain_cobalt_events(&mut test_fut);
8898        let logged_metrics =
8899            test_helper.get_logged_metrics(metrics::INTERFACE_DESTRUCTION_FAILURE_METRIC_ID);
8900        assert_eq!(logged_metrics.len(), 1);
8901    }
8902
8903    #[test_case(ScanIssue::ScanFailure, metrics::CLIENT_SCAN_FAILURE_METRIC_ID)]
8904    #[test_case(ScanIssue::AbortedScan, metrics::ABORTED_SCAN_METRIC_ID)]
8905    #[test_case(ScanIssue::EmptyScanResults, metrics::EMPTY_SCAN_RESULTS_METRIC_ID)]
8906    #[fuchsia::test(add_test_attr = false)]
8907    fn test_scan_defect_metrics(scan_issue: ScanIssue, expected_metric_id: u32) {
8908        let (mut test_helper, mut test_fut) = setup_test();
8909
8910        let event = TelemetryEvent::ScanEvent {
8911            inspect_data: ScanEventInspectData::new(),
8912            scan_defects: vec![scan_issue],
8913        };
8914
8915        // Send a notification that interface creation has failed.
8916        test_helper.telemetry_sender.send(event);
8917
8918        // Run the telemetry loop until it stalls.
8919        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8920
8921        // Expect that Cobalt has been notified of the metric
8922        test_helper.drain_cobalt_events(&mut test_fut);
8923        let logged_metrics = test_helper.get_logged_metrics(expected_metric_id);
8924        assert_eq!(logged_metrics.len(), 1);
8925    }
8926
8927    #[fuchsia::test]
8928    fn test_log_ap_start_failure() {
8929        let (mut test_helper, mut test_fut) = setup_test();
8930
8931        // Send a notification that starting the AP has failed.
8932        test_helper.telemetry_sender.send(TelemetryEvent::StartApResult(Err(())));
8933
8934        // Run the telemetry loop until it stalls.
8935        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8936
8937        // Expect that Cobalt has been notified of the AP start failure.
8938        test_helper.drain_cobalt_events(&mut test_fut);
8939        let logged_metrics = test_helper.get_logged_metrics(metrics::AP_START_FAILURE_METRIC_ID);
8940        assert_eq!(logged_metrics.len(), 1);
8941    }
8942
8943    #[test_case(
8944        RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset),
8945        metrics::RecoveryOccurrenceMetricDimensionReason::InterfaceCreationFailure ;
8946        "log recovery event for iface creation failure"
8947    )]
8948    #[test_case(
8949        RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset),
8950        metrics::RecoveryOccurrenceMetricDimensionReason::InterfaceDestructionFailure ;
8951        "log recovery event for iface destruction failure"
8952    )]
8953    #[test_case(
8954        RecoveryReason::ConnectFailure(ClientRecoveryMechanism::Disconnect),
8955        metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure ;
8956        "log recovery event for connect failure"
8957    )]
8958    #[test_case(
8959        RecoveryReason::StartApFailure(ApRecoveryMechanism::StopAp),
8960        metrics::RecoveryOccurrenceMetricDimensionReason::ApStartFailure ;
8961        "log recovery event for start AP failure"
8962    )]
8963    #[test_case(
8964        RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect),
8965        metrics::RecoveryOccurrenceMetricDimensionReason::ScanFailure ;
8966        "log recovery event for scan failure"
8967    )]
8968    #[test_case(
8969        RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect),
8970         metrics::RecoveryOccurrenceMetricDimensionReason::ScanCancellation ;
8971        "log recovery event for scan cancellation"
8972    )]
8973    #[test_case(
8974        RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect),
8975        metrics::RecoveryOccurrenceMetricDimensionReason::ScanResultsEmpty ;
8976        "log recovery event for empty scan results"
8977    )]
8978    #[fuchsia::test(add_test_attr = false)]
8979    fn test_log_recovery_occurrence(
8980        reason: RecoveryReason,
8981        expected_dimension: metrics::RecoveryOccurrenceMetricDimensionReason,
8982    ) {
8983        let (mut test_helper, mut test_fut) = setup_test();
8984
8985        // Send the recovery event metric.
8986        test_helper.telemetry_sender.send(TelemetryEvent::RecoveryEvent { reason });
8987
8988        // Run the telemetry loop until it stalls.
8989        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8990
8991        // Expect that Cobalt has been notified of the recovery event
8992        assert_matches!(
8993            test_helper.exec.run_until_stalled(&mut test_helper.cobalt_stream.next()),
8994            Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerRequest::LogOccurrence {
8995                metric_id, event_codes, responder, ..
8996            }))) => {
8997                assert_eq!(metric_id, metrics::RECOVERY_OCCURRENCE_METRIC_ID);
8998                assert_eq!(event_codes, vec![expected_dimension.as_event_code()]);
8999
9000                assert!(responder.send(Ok(())).is_ok());
9001        });
9002    }
9003
9004    #[test_case(
9005        RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset),
9006        RecoveryOutcome::Success,
9007        metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
9008        vec![RecoveryOutcome::Success as u32] ;
9009        "create iface fixed by resetting PHY"
9010    )]
9011    #[test_case(
9012        RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset),
9013        RecoveryOutcome::Failure,
9014        metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
9015        vec![RecoveryOutcome::Failure as u32] ;
9016        "create iface not fixed by resetting PHY"
9017    )]
9018    #[test_case(
9019        RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset),
9020        RecoveryOutcome::Success,
9021        metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
9022        vec![RecoveryOutcome::Success as u32] ;
9023        "destroy iface fixed by resetting PHY"
9024    )]
9025    #[test_case(
9026        RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset),
9027        RecoveryOutcome::Failure,
9028        metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
9029        vec![RecoveryOutcome::Failure as u32] ;
9030        "destroy iface not fixed by resetting PHY"
9031    )]
9032    #[test_case(
9033        RecoveryReason::ConnectFailure(ClientRecoveryMechanism::Disconnect),
9034        RecoveryOutcome::Success,
9035        metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9036        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9037        "connect works after disconnecting"
9038    )]
9039    #[test_case(
9040        RecoveryReason::ConnectFailure(ClientRecoveryMechanism::DestroyIface),
9041        RecoveryOutcome::Success,
9042        metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9043        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9044        "connect works after destroying iface"
9045    )]
9046    #[test_case(
9047        RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset),
9048        RecoveryOutcome::Success,
9049        metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9050        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9051        "connect works after resetting PHY"
9052    )]
9053    #[test_case(
9054        RecoveryReason::ConnectFailure(ClientRecoveryMechanism::Disconnect),
9055        RecoveryOutcome::Failure,
9056        metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9057        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9058        "connect still fails after disconnecting"
9059    )]
9060    #[test_case(
9061        RecoveryReason::ConnectFailure(ClientRecoveryMechanism::DestroyIface),
9062        RecoveryOutcome::Failure,
9063        metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9064        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9065        "connect still fails after destroying iface"
9066    )]
9067    #[test_case(
9068        RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset),
9069        RecoveryOutcome::Failure,
9070        metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9071        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9072        "connect still fails after resetting PHY"
9073    )]
9074    #[test_case(
9075        RecoveryReason::StartApFailure(ApRecoveryMechanism::StopAp),
9076        RecoveryOutcome::Success,
9077        metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9078        vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::StopAp as u32] ;
9079        "start AP works after stopping AP"
9080    )]
9081    #[test_case(
9082        RecoveryReason::StartApFailure(ApRecoveryMechanism::DestroyIface),
9083        RecoveryOutcome::Success,
9084        metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9085        vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::DestroyIface as u32] ;
9086        "start AP works after destroying iface"
9087    )]
9088    #[test_case(
9089        RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy),
9090        RecoveryOutcome::Success,
9091        metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9092        vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::ResetPhy as u32] ;
9093        "start AP works after resetting PHY"
9094    )]
9095    #[test_case(
9096        RecoveryReason::StartApFailure(ApRecoveryMechanism::StopAp),
9097        RecoveryOutcome::Failure,
9098        metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9099        vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::StopAp as u32] ;
9100        "start AP still fails after stopping AP"
9101    )]
9102    #[test_case(
9103        RecoveryReason::StartApFailure(ApRecoveryMechanism::DestroyIface),
9104        RecoveryOutcome::Failure,
9105        metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9106        vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::DestroyIface as u32] ;
9107        "start AP still fails after destroying iface"
9108    )]
9109    #[test_case(
9110        RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy),
9111        RecoveryOutcome::Failure,
9112        metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9113        vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::ResetPhy as u32] ;
9114        "start AP still fails after resetting PHY"
9115    )]
9116    #[test_case(
9117        RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect),
9118        RecoveryOutcome::Success,
9119        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9120        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9121        "scan works after disconnecting"
9122    )]
9123    #[test_case(
9124        RecoveryReason::ScanFailure(ClientRecoveryMechanism::DestroyIface),
9125        RecoveryOutcome::Success,
9126        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9127        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9128        "scan works after destroying iface"
9129    )]
9130    #[test_case(
9131        RecoveryReason::ScanFailure(ClientRecoveryMechanism::PhyReset),
9132        RecoveryOutcome::Success,
9133        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9134        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9135        "scan works after resetting PHY"
9136    )]
9137    #[test_case(
9138        RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect),
9139        RecoveryOutcome::Failure,
9140        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9141        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9142        "scan still fails after disconnecting"
9143    )]
9144    #[test_case(
9145        RecoveryReason::ScanFailure(ClientRecoveryMechanism::DestroyIface),
9146        RecoveryOutcome::Failure,
9147        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9148        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9149        "scan still fails after destroying iface"
9150    )]
9151    #[test_case(
9152        RecoveryReason::ScanFailure(ClientRecoveryMechanism::PhyReset),
9153        RecoveryOutcome::Failure,
9154        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9155        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9156        "scan still fails after resetting PHY"
9157    )]
9158    #[test_case(
9159        RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect),
9160        RecoveryOutcome::Success,
9161        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9162        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9163        "scan is no longer cancelled after disconnecting"
9164    )]
9165    #[test_case(
9166        RecoveryReason::ScanCancellation(ClientRecoveryMechanism::DestroyIface),
9167        RecoveryOutcome::Success,
9168        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9169        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9170        "scan is no longer cancelled after destroying iface"
9171    )]
9172    #[test_case(
9173        RecoveryReason::ScanCancellation(ClientRecoveryMechanism::PhyReset),
9174        RecoveryOutcome::Success,
9175        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9176        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9177        "scan is no longer cancelled after resetting PHY"
9178    )]
9179    #[test_case(
9180        RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect),
9181        RecoveryOutcome::Failure,
9182        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9183        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9184        "scan is still cancelled after disconnect"
9185    )]
9186    #[test_case(
9187        RecoveryReason::ScanCancellation(ClientRecoveryMechanism::DestroyIface),
9188        RecoveryOutcome::Failure,
9189        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9190        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9191        "scan is still cancelled after destroying iface"
9192    )]
9193    #[test_case(
9194        RecoveryReason::ScanCancellation(ClientRecoveryMechanism::PhyReset),
9195        RecoveryOutcome::Failure,
9196        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9197        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9198        "scan is still cancelled after resetting PHY"
9199    )]
9200    #[test_case(
9201        RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect),
9202        RecoveryOutcome::Success,
9203        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9204        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9205        "scan results not empty after disconnect"
9206    )]
9207    #[test_case(
9208        RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::DestroyIface),
9209        RecoveryOutcome::Success,
9210        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9211        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9212        "scan results not empty after destroy iface"
9213    )]
9214    #[test_case(
9215        RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::PhyReset),
9216        RecoveryOutcome::Success,
9217        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9218        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9219        "scan results not empty after PHY reset"
9220    )]
9221    #[test_case(
9222        RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect),
9223        RecoveryOutcome::Failure,
9224        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9225        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9226        "scan results still empty after disconnect"
9227    )]
9228    #[test_case(
9229        RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::DestroyIface),
9230        RecoveryOutcome::Failure,
9231        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9232        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9233        "scan results still empty after destroy iface"
9234    )]
9235    #[test_case(
9236        RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::PhyReset),
9237        RecoveryOutcome::Failure,
9238        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9239        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9240        "scan results still empty after PHY reset"
9241    )]
9242    #[fuchsia::test(add_test_attr = false)]
9243    fn test_log_post_recovery_result(
9244        reason: RecoveryReason,
9245        outcome: RecoveryOutcome,
9246        expected_metric_id: u32,
9247        expected_event_codes: Vec<u32>,
9248    ) {
9249        let mut exec = fasync::TestExecutor::new();
9250
9251        // Construct a StatsLogger
9252        let (cobalt_proxy, mut cobalt_stream) =
9253            create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
9254
9255        let inspector = Inspector::default();
9256        let inspect_node = inspector.root().create_child("stats");
9257
9258        let mut stats_logger = StatsLogger::new(cobalt_proxy, &inspect_node);
9259
9260        // Log the test telemetry event.
9261        let fut = stats_logger.log_post_recovery_result(reason, outcome);
9262        let mut fut = pin!(fut);
9263        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
9264
9265        // Verify the metric that was emitted.
9266        assert_matches!(
9267            exec.run_until_stalled(&mut cobalt_stream.next()),
9268            Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerRequest::LogOccurrence {
9269                metric_id, event_codes, responder, ..
9270            }))) => {
9271                assert_eq!(metric_id, expected_metric_id);
9272                assert_eq!(event_codes, expected_event_codes);
9273
9274                assert!(responder.send(Ok(())).is_ok());
9275        });
9276
9277        // The future should complete.
9278        assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
9279    }
9280
9281    #[fuchsia::test]
9282    fn test_post_recovery_connect_success() {
9283        let (mut test_helper, mut test_fut) = setup_test();
9284
9285        // Send the recovery event metric.
9286        let reason = RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset);
9287        let event = TelemetryEvent::RecoveryEvent { reason };
9288        test_helper.telemetry_sender.send(event);
9289
9290        // Run the telemetry loop until it stalls.
9291        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9292
9293        // Expect that Cobalt has been notified of the recovery event
9294        test_helper.drain_cobalt_events(&mut test_fut);
9295        let logged_metrics = test_helper.get_logged_metrics(metrics::RECOVERY_OCCURRENCE_METRIC_ID);
9296        assert_eq!(logged_metrics.len(), 1);
9297
9298        // Verify the reason dimension.
9299        assert_eq!(
9300            logged_metrics[0].event_codes,
9301            vec![
9302                metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure
9303                    .as_event_code()
9304            ]
9305        );
9306
9307        // Send a successful connect result.
9308        test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
9309            iface_id: IFACE_ID,
9310            policy_connect_reason: Some(
9311                client::types::ConnectReason::RetryAfterFailedConnectAttempt,
9312            ),
9313            result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
9314            multiple_bss_candidates: true,
9315            ap_state: random_bss_description!(Wpa1).into(),
9316            network_is_likely_hidden: false,
9317        });
9318
9319        // Run the telemetry loop until it stalls.
9320        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9321
9322        // Verify the connect post-recovery success metric was logged.
9323        test_helper.drain_cobalt_events(&mut test_fut);
9324        let logged_metrics =
9325            test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
9326        assert_eq!(
9327            logged_metrics[0].event_codes,
9328            vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32]
9329        );
9330
9331        // Verify a subsequent connect result does not cause another metric to be logged.
9332        test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
9333            iface_id: IFACE_ID,
9334            policy_connect_reason: Some(
9335                client::types::ConnectReason::RetryAfterFailedConnectAttempt,
9336            ),
9337            result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
9338            multiple_bss_candidates: true,
9339            ap_state: random_bss_description!(Wpa1).into(),
9340            network_is_likely_hidden: false,
9341        });
9342
9343        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9344
9345        test_helper.cobalt_events = Vec::new();
9346        test_helper.drain_cobalt_events(&mut test_fut);
9347        let logged_metrics =
9348            test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
9349        assert!(logged_metrics.is_empty());
9350    }
9351
9352    #[fuchsia::test]
9353    fn test_post_recovery_connect_failure() {
9354        let (mut test_helper, mut test_fut) = setup_test();
9355
9356        // Send the recovery event metric.
9357        let reason = RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset);
9358        let event = TelemetryEvent::RecoveryEvent { reason };
9359        test_helper.telemetry_sender.send(event);
9360
9361        // Run the telemetry loop until it stalls.
9362        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9363
9364        // Expect that Cobalt has been notified of the recovery event
9365        test_helper.drain_cobalt_events(&mut test_fut);
9366        let logged_metrics = test_helper.get_logged_metrics(metrics::RECOVERY_OCCURRENCE_METRIC_ID);
9367        assert_eq!(logged_metrics.len(), 1);
9368
9369        // Verify the reason dimension.
9370        assert_eq!(
9371            logged_metrics[0].event_codes,
9372            vec![
9373                metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure
9374                    .as_event_code()
9375            ]
9376        );
9377
9378        // Send a failed connect result.
9379        test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
9380            iface_id: IFACE_ID,
9381            policy_connect_reason: Some(
9382                client::types::ConnectReason::RetryAfterFailedConnectAttempt,
9383            ),
9384            result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
9385            multiple_bss_candidates: true,
9386            ap_state: random_bss_description!(Wpa1).into(),
9387            network_is_likely_hidden: false,
9388        });
9389
9390        // Run the telemetry loop until it stalls.
9391        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9392
9393        // Verify the connect post-recovery failure metric was logged.
9394        test_helper.drain_cobalt_events(&mut test_fut);
9395        let logged_metrics =
9396            test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
9397        assert_eq!(
9398            logged_metrics[0].event_codes,
9399            vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32]
9400        );
9401
9402        // Verify a subsequent connect result does not cause another metric to be logged.
9403        test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
9404            iface_id: IFACE_ID,
9405            policy_connect_reason: Some(
9406                client::types::ConnectReason::RetryAfterFailedConnectAttempt,
9407            ),
9408            result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
9409            multiple_bss_candidates: true,
9410            ap_state: random_bss_description!(Wpa1).into(),
9411            network_is_likely_hidden: false,
9412        });
9413
9414        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9415
9416        test_helper.cobalt_events = Vec::new();
9417        test_helper.drain_cobalt_events(&mut test_fut);
9418        let logged_metrics =
9419            test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
9420        assert!(logged_metrics.is_empty());
9421    }
9422
9423    fn test_generic_post_recovery_event(
9424        recovery_event: TelemetryEvent,
9425        post_recovery_event: TelemetryEvent,
9426        duplicate_check_event: TelemetryEvent,
9427        expected_metric_id: u32,
9428        dimensions: Vec<u32>,
9429    ) {
9430        let (mut test_helper, mut test_fut) = setup_test();
9431        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(1));
9432
9433        // Send the recovery event metric
9434        test_helper.telemetry_sender.send(recovery_event);
9435        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9436
9437        // Send the post-recovery result metric
9438        test_helper.telemetry_sender.send(post_recovery_event);
9439        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9440
9441        // Get the metric that was logged and verify that it was constructed properly.
9442        test_helper.drain_cobalt_events(&mut test_fut);
9443        let logged_metrics = test_helper.get_logged_metrics(expected_metric_id);
9444
9445        assert_eq!(logged_metrics.len(), 1);
9446        assert_eq!(logged_metrics[0].event_codes, dimensions);
9447
9448        // Re-send the result metric and verify that nothing new was logged.
9449        test_helper.cobalt_events = Vec::new();
9450        test_helper.telemetry_sender.send(duplicate_check_event);
9451        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9452        let logged_metrics = test_helper.get_logged_metrics(expected_metric_id);
9453        assert!(logged_metrics.is_empty());
9454
9455        // If the recovery was successful, ensure that the last successful recovery time has been
9456        // updated.  If it was not successful, the last recovery time should not have been changed.
9457        if dimensions[0] == RecoveryOutcome::Success.as_event_code() {
9458            assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
9459                stats: contains {
9460                    last_successful_recovery: 1_u64,
9461                    successful_recoveries: 1_u64
9462                }
9463            });
9464        } else {
9465            assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
9466                stats: contains {
9467                    last_successful_recovery: 0_u64,
9468                    successful_recoveries: 0_u64
9469                }
9470            });
9471        }
9472    }
9473
9474    #[test_case(
9475        TelemetryEvent::RecoveryEvent {
9476            reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect)
9477        },
9478        TelemetryEvent::ScanEvent {
9479            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9480            scan_defects: vec![]
9481        },
9482        TelemetryEvent::ScanEvent {
9483            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9484            scan_defects: vec![]
9485        },
9486        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9487        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9488        "Scan succeeds after recovery with no other defects"
9489    )]
9490    #[test_case(
9491        TelemetryEvent::RecoveryEvent {
9492            reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect)
9493        },
9494        TelemetryEvent::ScanEvent {
9495            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9496            scan_defects: vec![ScanIssue::ScanFailure]
9497        },
9498        TelemetryEvent::ScanEvent {
9499            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9500            scan_defects: vec![ScanIssue::ScanFailure]
9501        },
9502        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9503        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9504        "Scan still fails following recovery"
9505    )]
9506    #[test_case(
9507        TelemetryEvent::RecoveryEvent {
9508            reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::DestroyIface)
9509        },
9510        TelemetryEvent::ScanEvent {
9511            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9512            scan_defects: vec![ScanIssue::AbortedScan]
9513        },
9514        TelemetryEvent::ScanEvent {
9515            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9516            scan_defects: vec![ScanIssue::AbortedScan]
9517        },
9518        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9519        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9520        "Scan succeeds after recovery but the scan was cancelled"
9521    )]
9522    #[test_case(
9523        TelemetryEvent::RecoveryEvent {
9524            reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::PhyReset)
9525        },
9526        TelemetryEvent::ScanEvent {
9527            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9528            scan_defects: vec![ScanIssue::EmptyScanResults]
9529        },
9530        TelemetryEvent::ScanEvent {
9531            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9532            scan_defects: vec![ScanIssue::EmptyScanResults]
9533        },
9534        metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9535        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9536        "Scan succeeds after recovery but the results are empty"
9537    )]
9538    #[test_case(
9539        TelemetryEvent::RecoveryEvent {
9540            reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect)
9541        },
9542        TelemetryEvent::ScanEvent {
9543            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9544            scan_defects: vec![]
9545        },
9546        TelemetryEvent::ScanEvent {
9547            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9548            scan_defects: vec![]
9549        },
9550        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9551        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9552        "Scan no longer cancelled after recovery"
9553    )]
9554    #[test_case(
9555        TelemetryEvent::RecoveryEvent {
9556            reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect)
9557        },
9558        TelemetryEvent::ScanEvent {
9559            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9560            scan_defects: vec![ScanIssue::ScanFailure]
9561        },
9562        TelemetryEvent::ScanEvent {
9563            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9564            scan_defects: vec![ScanIssue::ScanFailure]
9565        },
9566        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9567        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9568        "Scan not cancelled after recovery but fails instead"
9569    )]
9570    #[test_case(
9571        TelemetryEvent::RecoveryEvent {
9572            reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::DestroyIface)
9573        },
9574        TelemetryEvent::ScanEvent {
9575            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9576            scan_defects: vec![ScanIssue::AbortedScan]
9577        },
9578        TelemetryEvent::ScanEvent {
9579            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9580            scan_defects: vec![ScanIssue::AbortedScan]
9581        },
9582        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9583        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9584        "Scan still cancelled after recovery"
9585    )]
9586    #[test_case(
9587        TelemetryEvent::RecoveryEvent {
9588            reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::PhyReset)
9589        },
9590        TelemetryEvent::ScanEvent {
9591            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9592            scan_defects: vec![ScanIssue::EmptyScanResults]
9593        },
9594        TelemetryEvent::ScanEvent {
9595            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9596            scan_defects: vec![ScanIssue::EmptyScanResults]
9597        },
9598        metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9599        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9600        "Scan not cancelled after recovery but results are empty"
9601    )]
9602    #[test_case(
9603        TelemetryEvent::RecoveryEvent {
9604            reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect)
9605        },
9606        TelemetryEvent::ScanEvent {
9607            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9608            scan_defects: vec![]
9609        },
9610        TelemetryEvent::ScanEvent {
9611            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9612            scan_defects: vec![]
9613        },
9614        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9615        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9616        "Scan results not empty after recovery and no other errors"
9617    )]
9618    #[test_case(
9619        TelemetryEvent::RecoveryEvent {
9620            reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect)
9621        },
9622        TelemetryEvent::ScanEvent {
9623            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9624            scan_defects: vec![ScanIssue::ScanFailure]
9625        },
9626        TelemetryEvent::ScanEvent {
9627            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9628            scan_defects: vec![ScanIssue::ScanFailure]
9629        },
9630        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9631        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9632        "Scan results no longer empty after recovery, but scan fails"
9633    )]
9634    #[test_case(
9635        TelemetryEvent::RecoveryEvent {
9636            reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::DestroyIface)
9637        },
9638        TelemetryEvent::ScanEvent {
9639            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9640            scan_defects: vec![ScanIssue::AbortedScan]
9641        },
9642        TelemetryEvent::ScanEvent {
9643            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9644            scan_defects: vec![ScanIssue::AbortedScan]
9645        },
9646        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9647        vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9648        "Scan results not empty after recovery but scan is cancelled"
9649    )]
9650    #[test_case(
9651        TelemetryEvent::RecoveryEvent {
9652            reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::PhyReset)
9653        },
9654        TelemetryEvent::ScanEvent {
9655            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9656            scan_defects: vec![ScanIssue::EmptyScanResults]
9657        },
9658        TelemetryEvent::ScanEvent {
9659            inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9660            scan_defects: vec![ScanIssue::EmptyScanResults]
9661        },
9662        metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9663        vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9664        "Scan results still empty after recovery"
9665    )]
9666    #[fuchsia::test(add_test_attr = false)]
9667    fn test_post_recovery_scan_metrics(
9668        recovery_event: TelemetryEvent,
9669        post_recovery_event: TelemetryEvent,
9670        duplicate_check_event: TelemetryEvent,
9671        expected_metric_id: u32,
9672        dimensions: Vec<u32>,
9673    ) {
9674        test_generic_post_recovery_event(
9675            recovery_event,
9676            post_recovery_event,
9677            duplicate_check_event,
9678            expected_metric_id,
9679            dimensions,
9680        );
9681    }
9682
9683    #[test_case(
9684        TelemetryEvent::RecoveryEvent {
9685            reason: RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy)
9686        },
9687        TelemetryEvent::StartApResult(Err(())),
9688        TelemetryEvent::StartApResult(Err(())),
9689        metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9690        vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::ResetPhy as u32] ;
9691        "start AP still does not work after recovery"
9692    )]
9693    #[test_case(
9694        TelemetryEvent::RecoveryEvent {
9695            reason: RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy)
9696        },
9697        TelemetryEvent::StartApResult(Ok(())),
9698        TelemetryEvent::StartApResult(Ok(())),
9699        metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9700        vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::ResetPhy as u32] ;
9701        "start AP works after recovery"
9702    )]
9703    #[fuchsia::test(add_test_attr = false)]
9704    fn test_post_recovery_start_ap(
9705        recovery_event: TelemetryEvent,
9706        post_recovery_event: TelemetryEvent,
9707        duplicate_check_event: TelemetryEvent,
9708        expected_metric_id: u32,
9709        dimensions: Vec<u32>,
9710    ) {
9711        test_generic_post_recovery_event(
9712            recovery_event,
9713            post_recovery_event,
9714            duplicate_check_event,
9715            expected_metric_id,
9716            dimensions,
9717        );
9718    }
9719
9720    #[test_case(
9721        TelemetryEvent::RecoveryEvent {
9722            reason: RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset)
9723        },
9724        TelemetryEvent::IfaceCreationResult(Err(())),
9725        TelemetryEvent::IfaceCreationResult(Err(())),
9726        metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
9727        vec![RecoveryOutcome::Failure as u32] ;
9728        "create iface still does not work after recovery"
9729    )]
9730    #[test_case(
9731        TelemetryEvent::RecoveryEvent {
9732            reason: RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset)
9733        },
9734        TelemetryEvent::IfaceCreationResult(Ok(())),
9735        TelemetryEvent::IfaceCreationResult(Ok(())),
9736        metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
9737        vec![RecoveryOutcome::Success as u32] ;
9738        "create iface works after recovery"
9739    )]
9740    #[fuchsia::test(add_test_attr = false)]
9741    fn test_post_recovery_create_iface(
9742        recovery_event: TelemetryEvent,
9743        post_recovery_event: TelemetryEvent,
9744        duplicate_check_event: TelemetryEvent,
9745        expected_metric_id: u32,
9746        dimensions: Vec<u32>,
9747    ) {
9748        test_generic_post_recovery_event(
9749            recovery_event,
9750            post_recovery_event,
9751            duplicate_check_event,
9752            expected_metric_id,
9753            dimensions,
9754        );
9755    }
9756
9757    #[test_case(
9758        TelemetryEvent::RecoveryEvent {
9759            reason: RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset)
9760        },
9761        TelemetryEvent::IfaceDestructionResult(Err(())),
9762        TelemetryEvent::IfaceDestructionResult(Err(())),
9763        metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
9764        vec![RecoveryOutcome::Failure as u32] ;
9765        "destroy iface does not work after recovery"
9766    )]
9767    #[test_case(
9768        TelemetryEvent::RecoveryEvent {
9769            reason: RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset)
9770        },
9771        TelemetryEvent::IfaceDestructionResult(Ok(())),
9772        TelemetryEvent::IfaceDestructionResult(Ok(())),
9773        metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
9774        vec![RecoveryOutcome::Success as u32] ;
9775        "destroy iface works after recovery"
9776    )]
9777    #[fuchsia::test(add_test_attr = false)]
9778    fn test_post_recovery_destroy_iface(
9779        recovery_event: TelemetryEvent,
9780        post_recovery_event: TelemetryEvent,
9781        duplicate_check_event: TelemetryEvent,
9782        expected_metric_id: u32,
9783        dimensions: Vec<u32>,
9784    ) {
9785        test_generic_post_recovery_event(
9786            recovery_event,
9787            post_recovery_event,
9788            duplicate_check_event,
9789            expected_metric_id,
9790            dimensions,
9791        );
9792    }
9793
9794    #[test_case(
9795        TelemetryEvent::RecoveryEvent {
9796            reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9797        },
9798        TelemetryEvent::ConnectResult {
9799            iface_id: IFACE_ID,
9800            policy_connect_reason: Some(
9801                client::types::ConnectReason::RetryAfterFailedConnectAttempt,
9802            ),
9803            result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
9804            multiple_bss_candidates: true,
9805            ap_state: random_bss_description!(Wpa2).into(),
9806            network_is_likely_hidden: true,
9807        },
9808        TelemetryEvent::ConnectResult {
9809            iface_id: IFACE_ID,
9810            policy_connect_reason: Some(
9811                client::types::ConnectReason::RetryAfterFailedConnectAttempt,
9812            ),
9813            result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
9814            multiple_bss_candidates: true,
9815            ap_state: random_bss_description!(Wpa2).into(),
9816            network_is_likely_hidden: true,
9817        },
9818        metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9819        vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9820        "Connect works after recovery"
9821    )]
9822    #[test_case(
9823        TelemetryEvent::RecoveryEvent {
9824            reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9825        },
9826        TelemetryEvent::Disconnected {
9827            track_subsequent_downtime: false,
9828            info: Some(fake_disconnect_info()),
9829        },
9830        TelemetryEvent::Disconnected {
9831            track_subsequent_downtime: false,
9832            info: Some(fake_disconnect_info()),
9833        },
9834        metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9835        vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9836        "Disconnect works after recovery"
9837    )]
9838    #[test_case(
9839        TelemetryEvent::RecoveryEvent {
9840            reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9841        },
9842        TelemetryEvent::StopAp { enabled_duration: zx::MonotonicDuration::from_seconds(0) },
9843        TelemetryEvent::StopAp { enabled_duration: zx::MonotonicDuration::from_seconds(0) },
9844        metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9845        vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9846        "Stop AP works after recovery"
9847    )]
9848    #[test_case(
9849        TelemetryEvent::RecoveryEvent {
9850            reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9851        },
9852        TelemetryEvent::StartApResult(Ok(())),
9853        TelemetryEvent::StartApResult(Ok(())),
9854        metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9855        vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9856        "Start AP works after recovery"
9857    )]
9858    #[test_case(
9859        TelemetryEvent::RecoveryEvent {
9860            reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::DestroyIface)
9861        },
9862        TelemetryEvent::ScanEvent {
9863            inspect_data: ScanEventInspectData::default(),
9864            scan_defects: vec![]
9865        },
9866        TelemetryEvent::ScanEvent {
9867            inspect_data: ScanEventInspectData::default(),
9868            scan_defects: vec![]
9869        },
9870        metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9871        vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::DestroyIface as u32] ;
9872        "Scan works after timeout recovery"
9873    )]
9874    #[test_case(
9875        TelemetryEvent::RecoveryEvent {
9876            reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9877        },
9878        TelemetryEvent::SmeTimeout { source: TimeoutSource::Scan },
9879        TelemetryEvent::SmeTimeout { source: TimeoutSource::Scan },
9880        metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9881        vec![RecoveryOutcome::Failure as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9882        "SME timeout after recovery"
9883    )]
9884    #[fuchsia::test(add_test_attr = false)]
9885    fn test_post_recovery_timeout(
9886        recovery_event: TelemetryEvent,
9887        post_recovery_event: TelemetryEvent,
9888        duplicate_check_event: TelemetryEvent,
9889        expected_metric_id: u32,
9890        dimensions: Vec<u32>,
9891    ) {
9892        test_generic_post_recovery_event(
9893            recovery_event,
9894            post_recovery_event,
9895            duplicate_check_event,
9896            expected_metric_id,
9897            dimensions,
9898        );
9899    }
9900
9901    #[fuchsia::test]
9902    fn test_log_scan_request_fulfillment_time() {
9903        let (mut test_helper, mut test_fut) = setup_test();
9904
9905        // Send a scan fulfillment duration
9906        let duration = zx::MonotonicDuration::from_seconds(15);
9907        test_helper.telemetry_sender.send(TelemetryEvent::ScanRequestFulfillmentTime {
9908            duration,
9909            reason: client::scan::ScanReason::ClientRequest,
9910        });
9911
9912        // Run the telemetry loop until it stalls.
9913        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9914
9915        // Expect that Cobalt has been notified of the scan fulfillment metric
9916        test_helper.drain_cobalt_events(&mut test_fut);
9917        let logged_metrics = test_helper
9918            .get_logged_metrics(metrics::SUCCESSFUL_SCAN_REQUEST_FULFILLMENT_TIME_METRIC_ID);
9919        assert_eq!(logged_metrics.len(), 1);
9920        assert_eq!(
9921            logged_metrics[0].event_codes,
9922            vec![
9923                metrics::ConnectivityWlanMetricDimensionScanFulfillmentTime::LessThanTwentyOneSeconds as u32,
9924                metrics::ConnectivityWlanMetricDimensionScanReason::ClientRequest as u32
9925            ]
9926        );
9927    }
9928
9929    #[fuchsia::test]
9930    fn test_log_scan_queue_statistics() {
9931        let (mut test_helper, mut test_fut) = setup_test();
9932
9933        // Send a scan queue report
9934        test_helper.telemetry_sender.send(TelemetryEvent::ScanQueueStatistics {
9935            fulfilled_requests: 4,
9936            remaining_requests: 12,
9937        });
9938
9939        // Run the telemetry loop until it stalls.
9940        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9941
9942        // Expect that Cobalt has been notified of the scan queue metrics
9943        test_helper.drain_cobalt_events(&mut test_fut);
9944        let logged_metrics = test_helper
9945            .get_logged_metrics(metrics::SCAN_QUEUE_STATISTICS_AFTER_COMPLETED_SCAN_METRIC_ID);
9946        assert_eq!(logged_metrics.len(), 1);
9947        assert_eq!(
9948            logged_metrics[0].event_codes,
9949            vec![
9950                metrics::ConnectivityWlanMetricDimensionScanRequestsFulfilled::Four as u32,
9951                metrics::ConnectivityWlanMetricDimensionScanRequestsRemaining::TenToFourteen as u32
9952            ]
9953        );
9954    }
9955
9956    #[fuchsia::test]
9957    fn test_log_post_connection_score_deltas_by_signal_and_post_connection_rssi_deltas() {
9958        let (mut test_helper, mut test_fut) = setup_test();
9959        let connect_time = fasync::MonotonicInstant::from_nanos(31_000_000_000);
9960
9961        let signals_deque: VecDeque<client::types::TimestampedSignal> = VecDeque::from_iter([
9962            client::types::TimestampedSignal {
9963                signal: client::types::Signal { rssi_dbm: -70, snr_db: 10 },
9964                time: connect_time + zx::MonotonicDuration::from_millis(500),
9965            },
9966            client::types::TimestampedSignal {
9967                signal: client::types::Signal { rssi_dbm: -50, snr_db: 30 },
9968                time: connect_time + zx::MonotonicDuration::from_seconds(4),
9969            },
9970            client::types::TimestampedSignal {
9971                signal: client::types::Signal { rssi_dbm: -30, snr_db: 60 },
9972                time: connect_time + zx::MonotonicDuration::from_seconds(9),
9973            },
9974            client::types::TimestampedSignal {
9975                signal: client::types::Signal { rssi_dbm: -10, snr_db: 80 },
9976                time: connect_time + zx::MonotonicDuration::from_seconds(20),
9977            },
9978        ]);
9979        let signals = HistoricalList(signals_deque);
9980        let signal_at_connect = client::types::Signal { rssi_dbm: -90, snr_db: 0 };
9981
9982        test_helper.telemetry_sender.send(TelemetryEvent::PostConnectionSignals {
9983            connect_time,
9984            signal_at_connect,
9985            signals,
9986        });
9987
9988        // Catch logged score delta metrics
9989        test_helper.drain_cobalt_events(&mut test_fut);
9990        let logged_metrics = test_helper.get_logged_metrics(
9991            metrics::AVERAGE_SCORE_DELTA_AFTER_CONNECTION_BY_INITIAL_SCORE_METRIC_ID,
9992        );
9993
9994        use metrics::AverageScoreDeltaAfterConnectionByInitialScoreMetricDimensionTimeSinceConnect as DurationDimension;
9995
9996        // Logged metrics for one, five, ten, and thirty seconds.
9997        assert_eq!(logged_metrics.len(), 4);
9998
9999        let mut prev_score = 0;
10000        // Verify one second average delta
10001        assert_eq!(logged_metrics[0].event_codes[1], DurationDimension::OneSecond as u32);
10002        assert_matches!(&logged_metrics[0].payload, MetricEventPayload::IntegerValue(delta) => {
10003            assert_gt!(*delta, prev_score);
10004            prev_score = *delta;
10005        });
10006
10007        // Verify five second average delta
10008        assert_eq!(logged_metrics[1].event_codes[1], DurationDimension::FiveSeconds as u32);
10009        assert_matches!(&logged_metrics[1].payload, MetricEventPayload::IntegerValue(delta) => {
10010            assert_gt!(*delta, prev_score);
10011            prev_score = *delta;
10012        });
10013        // Verify ten second average delta
10014        assert_eq!(logged_metrics[2].event_codes[1], DurationDimension::TenSeconds as u32);
10015        assert_matches!(&logged_metrics[2].payload, MetricEventPayload::IntegerValue(delta) => {
10016            assert_gt!(*delta, prev_score);
10017            prev_score = *delta;
10018        });
10019        // Verify thirty second average delta
10020        assert_eq!(logged_metrics[3].event_codes[1], DurationDimension::ThirtySeconds as u32);
10021        assert_matches!(&logged_metrics[3].payload, MetricEventPayload::IntegerValue(delta) => {
10022            assert_gt!(*delta, prev_score);
10023        });
10024
10025        // Catch logged RSSI delta metrics
10026        test_helper.drain_cobalt_events(&mut test_fut);
10027        let logged_metrics = test_helper.get_logged_metrics(
10028            metrics::AVERAGE_RSSI_DELTA_AFTER_CONNECTION_BY_INITIAL_RSSI_METRIC_ID,
10029        );
10030        // Logged metrics for one, five, ten, and thirty seconds.
10031        assert_eq!(logged_metrics.len(), 4);
10032
10033        // Verify one second average RSSI delta
10034        assert_eq!(logged_metrics[0].event_codes[1], DurationDimension::OneSecond as u32);
10035        assert_matches!(&logged_metrics[0].payload, MetricEventPayload::IntegerValue(delta) => {
10036            assert_eq!(*delta, 10);
10037        });
10038
10039        // Verify five second average RSSI delta
10040        assert_eq!(logged_metrics[1].event_codes[1], DurationDimension::FiveSeconds as u32);
10041        assert_matches!(&logged_metrics[1].payload, MetricEventPayload::IntegerValue(delta) => {
10042            assert_eq!(*delta, 20);
10043        });
10044        // Verify ten second average RSSI delta
10045        assert_eq!(logged_metrics[2].event_codes[1], DurationDimension::TenSeconds as u32);
10046        assert_matches!(&logged_metrics[2].payload, MetricEventPayload::IntegerValue(delta) => {
10047            assert_eq!(*delta, 30);
10048        });
10049        // Verify thirty second average RSSI delta
10050        assert_eq!(logged_metrics[3].event_codes[1], DurationDimension::ThirtySeconds as u32);
10051        assert_matches!(&logged_metrics[3].payload, MetricEventPayload::IntegerValue(delta) => {
10052            assert_eq!(*delta, 40);
10053        });
10054    }
10055
10056    #[fuchsia::test]
10057    fn test_log_pre_disconnect_score_deltas_by_signal_and_pre_disconnect_rssi_deltas() {
10058        let (mut test_helper, mut test_fut) = setup_test();
10059        // 31 seconds
10060        let final_score_time = fasync::MonotonicInstant::from_nanos(31_000_000_000);
10061
10062        let signals_deque: VecDeque<client::types::TimestampedSignal> = VecDeque::from_iter([
10063            client::types::TimestampedSignal {
10064                signal: client::types::Signal { rssi_dbm: -10, snr_db: 80 },
10065                time: final_score_time - zx::MonotonicDuration::from_seconds(20),
10066            },
10067            client::types::TimestampedSignal {
10068                signal: client::types::Signal { rssi_dbm: -30, snr_db: 60 },
10069                time: final_score_time - zx::MonotonicDuration::from_seconds(9),
10070            },
10071            client::types::TimestampedSignal {
10072                signal: client::types::Signal { rssi_dbm: -50, snr_db: 30 },
10073                time: final_score_time - zx::MonotonicDuration::from_seconds(4),
10074            },
10075            client::types::TimestampedSignal {
10076                signal: client::types::Signal { rssi_dbm: -70, snr_db: 10 },
10077                time: final_score_time - zx::MonotonicDuration::from_millis(500),
10078            },
10079            client::types::TimestampedSignal {
10080                signal: client::types::Signal { rssi_dbm: -90, snr_db: 0 },
10081                time: final_score_time,
10082            },
10083        ]);
10084        let signals = HistoricalList(signals_deque);
10085
10086        let disconnect_info = DisconnectInfo {
10087            connected_duration: AVERAGE_SCORE_DELTA_MINIMUM_DURATION,
10088            signals,
10089            ..fake_disconnect_info()
10090        };
10091        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
10092            track_subsequent_downtime: false,
10093            info: Some(disconnect_info),
10094        });
10095
10096        // Catch logged score delta metrics
10097        test_helper.drain_cobalt_events(&mut test_fut);
10098        let logged_metrics = test_helper.get_logged_metrics(
10099            metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
10100        );
10101
10102        use metrics::AverageScoreDeltaBeforeDisconnectByFinalScoreMetricDimensionTimeUntilDisconnect as DurationDimension;
10103
10104        // Logged metrics for one, five, ten, and thirty seconds.
10105        assert_eq!(logged_metrics.len(), 4);
10106
10107        let mut prev_score = 0;
10108        // Verify one second average delta
10109        assert_eq!(logged_metrics[0].event_codes[1], DurationDimension::OneSecond as u32);
10110        assert_matches!(&logged_metrics[0].payload, MetricEventPayload::IntegerValue(delta) => {
10111            assert_gt!(*delta, prev_score);
10112            prev_score = *delta;
10113        });
10114
10115        // Verify five second average delta
10116        assert_eq!(logged_metrics[1].event_codes[1], DurationDimension::FiveSeconds as u32);
10117        assert_matches!(&logged_metrics[1].payload, MetricEventPayload::IntegerValue(delta) => {
10118            assert_gt!(*delta, prev_score);
10119            prev_score = *delta;
10120        });
10121        // Verify ten second average delta
10122        assert_eq!(logged_metrics[2].event_codes[1], DurationDimension::TenSeconds as u32);
10123        assert_matches!(&logged_metrics[2].payload, MetricEventPayload::IntegerValue(delta) => {
10124            assert_gt!(*delta, prev_score);
10125            prev_score = *delta;
10126        });
10127        // Verify thirty second average delta
10128        assert_eq!(logged_metrics[3].event_codes[1], DurationDimension::ThirtySeconds as u32);
10129        assert_matches!(&logged_metrics[3].payload, MetricEventPayload::IntegerValue(delta) => {
10130            assert_gt!(*delta, prev_score);
10131        });
10132
10133        // Catch logged RSSI delta metrics
10134        test_helper.drain_cobalt_events(&mut test_fut);
10135        let logged_metrics = test_helper.get_logged_metrics(
10136            metrics::AVERAGE_RSSI_DELTA_BEFORE_DISCONNECT_BY_FINAL_RSSI_METRIC_ID,
10137        );
10138        // Logged metrics for one, five, ten, and thirty seconds.
10139        assert_eq!(logged_metrics.len(), 4);
10140
10141        // Verify one second average RSSI delta
10142        assert_eq!(logged_metrics[0].event_codes[1], DurationDimension::OneSecond as u32);
10143        assert_matches!(&logged_metrics[0].payload, MetricEventPayload::IntegerValue(delta) => {
10144            assert_eq!(*delta, 10);
10145        });
10146
10147        // Verify five second average RSSI delta
10148        assert_eq!(logged_metrics[1].event_codes[1], DurationDimension::FiveSeconds as u32);
10149        assert_matches!(&logged_metrics[1].payload, MetricEventPayload::IntegerValue(delta) => {
10150            assert_eq!(*delta, 20);
10151        });
10152        // Verify ten second average RSSI delta
10153        assert_eq!(logged_metrics[2].event_codes[1], DurationDimension::TenSeconds as u32);
10154        assert_matches!(&logged_metrics[2].payload, MetricEventPayload::IntegerValue(delta) => {
10155            assert_eq!(*delta, 30);
10156        });
10157        // Verify thirty second average RSSI delta
10158        assert_eq!(logged_metrics[3].event_codes[1], DurationDimension::ThirtySeconds as u32);
10159        assert_matches!(&logged_metrics[3].payload, MetricEventPayload::IntegerValue(delta) => {
10160            assert_eq!(*delta, 40);
10161        });
10162
10163        // Record a disconnect shorter than the minimum required duration
10164        let disconnect_info = DisconnectInfo {
10165            connected_duration: AVERAGE_SCORE_DELTA_MINIMUM_DURATION
10166                - zx::MonotonicDuration::from_seconds(1),
10167            ..fake_disconnect_info()
10168        };
10169        test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
10170            track_subsequent_downtime: false,
10171            info: Some(disconnect_info),
10172        });
10173        test_helper.drain_cobalt_events(&mut test_fut);
10174
10175        // No additional metrics should be logged.
10176        let logged_metrics = test_helper.get_logged_metrics(
10177            metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
10178        );
10179        assert_eq!(logged_metrics.len(), 4);
10180        let logged_metrics = test_helper.get_logged_metrics(
10181            metrics::AVERAGE_RSSI_DELTA_BEFORE_DISCONNECT_BY_FINAL_RSSI_METRIC_ID,
10182        );
10183        assert_eq!(logged_metrics.len(), 4);
10184    }
10185
10186    #[fuchsia::test]
10187    fn test_log_network_selection_metrics() {
10188        let (mut test_helper, mut test_fut) = setup_test();
10189
10190        // Send network selection event
10191        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
10192            network_selection_type: NetworkSelectionType::Undirected,
10193            num_candidates: Ok(3),
10194            selected_count: 2,
10195        });
10196
10197        // Run the telemetry loop until it stalls.
10198        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
10199        test_helper.drain_cobalt_events(&mut test_fut);
10200
10201        // Verify the network selection is counted
10202        let logged_metrics =
10203            test_helper.get_logged_metrics(metrics::NETWORK_SELECTION_COUNT_METRIC_ID);
10204        assert_eq!(logged_metrics.len(), 1);
10205        assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
10206
10207        // Verify the number of selected candidates is recorded
10208        let logged_metrics =
10209            test_helper.get_logged_metrics(metrics::NUM_NETWORKS_SELECTED_METRIC_ID);
10210        assert_eq!(logged_metrics.len(), 1);
10211        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(2));
10212
10213        // Send a network selection metric where there were 0 candidates.
10214        test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
10215            network_selection_type: NetworkSelectionType::Undirected,
10216            num_candidates: Ok(0),
10217            selected_count: 0,
10218        });
10219
10220        // Run the telemetry loop until it stalls.
10221        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
10222        test_helper.drain_cobalt_events(&mut test_fut);
10223
10224        // Verify the network selection is counted
10225        let logged_metrics =
10226            test_helper.get_logged_metrics(metrics::NETWORK_SELECTION_COUNT_METRIC_ID);
10227        assert_eq!(logged_metrics.len(), 2);
10228
10229        // The number of selected networks should not be recorded, since there were no candidates
10230        // to select from
10231        let logged_metrics =
10232            test_helper.get_logged_metrics(metrics::NUM_NETWORKS_SELECTED_METRIC_ID);
10233        assert_eq!(logged_metrics.len(), 1);
10234    }
10235
10236    #[fuchsia::test]
10237    fn test_log_bss_selection_metrics() {
10238        let (mut test_helper, mut test_fut) = setup_test();
10239
10240        // Send BSS selection result event with 3 candidate, multi-bss, one selected
10241        let selected_candidate_2g = client::types::ScannedCandidate {
10242            bss: client::types::Bss {
10243                channel: client::types::WlanChan::new(1, wlan_common::channel::Cbw::Cbw20),
10244                ..generate_random_bss()
10245            },
10246            ..generate_random_scanned_candidate()
10247        };
10248        let candidate_2g = client::types::ScannedCandidate {
10249            bss: client::types::Bss {
10250                channel: client::types::WlanChan::new(1, wlan_common::channel::Cbw::Cbw20),
10251                ..generate_random_bss()
10252            },
10253            ..generate_random_scanned_candidate()
10254        };
10255        let candidate_5g = client::types::ScannedCandidate {
10256            bss: client::types::Bss {
10257                channel: client::types::WlanChan::new(36, wlan_common::channel::Cbw::Cbw40),
10258                ..generate_random_bss()
10259            },
10260            ..generate_random_scanned_candidate()
10261        };
10262        let scored_candidates =
10263            vec![(selected_candidate_2g.clone(), 70), (candidate_2g, 60), (candidate_5g, 50)];
10264
10265        test_helper.telemetry_sender.send(TelemetryEvent::BssSelectionResult {
10266            reason: client::types::ConnectReason::FidlConnectRequest,
10267            scored_candidates: scored_candidates.clone(),
10268            selected_candidate: Some((selected_candidate_2g, 70)),
10269        });
10270
10271        test_helper.drain_cobalt_events(&mut test_fut);
10272
10273        let fidl_connect_event_code = vec![
10274            metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::FidlConnectRequest
10275                as u32,
10276        ];
10277        // Check that the BSS selection occurrence metrics are logged
10278        let logged_metrics = test_helper.get_logged_metrics(metrics::BSS_SELECTION_COUNT_METRIC_ID);
10279        assert_eq!(logged_metrics.len(), 1);
10280        assert_eq!(logged_metrics[0].event_codes, Vec::<u32>::new());
10281        assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
10282
10283        let logged_metrics =
10284            test_helper.get_logged_metrics(metrics::BSS_SELECTION_COUNT_DETAILED_METRIC_ID);
10285        assert_eq!(logged_metrics.len(), 1);
10286        assert_eq!(logged_metrics[0].event_codes, fidl_connect_event_code);
10287        assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
10288
10289        // Check that the candidate count metrics are logged
10290        let logged_metrics =
10291            test_helper.get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_METRIC_ID);
10292        assert_eq!(logged_metrics.len(), 1);
10293        assert_eq!(logged_metrics[0].event_codes, Vec::<u32>::new());
10294        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(3));
10295
10296        let logged_metrics = test_helper
10297            .get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_DETAILED_METRIC_ID);
10298        assert_eq!(logged_metrics.len(), 1);
10299        assert_eq!(logged_metrics[0].event_codes, fidl_connect_event_code);
10300        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(3));
10301
10302        // Check that all candidate scores are logged
10303        let logged_metrics = test_helper.get_logged_metrics(metrics::BSS_CANDIDATE_SCORE_METRIC_ID);
10304        assert_eq!(logged_metrics.len(), 3);
10305        for i in 0..3 {
10306            assert_eq!(
10307                logged_metrics[i].payload,
10308                MetricEventPayload::IntegerValue(scored_candidates[i].1 as i64)
10309            )
10310        }
10311
10312        // Check that unique network count is logged
10313        let logged_metrics = test_helper
10314            .get_logged_metrics(metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID);
10315        assert_eq!(logged_metrics.len(), 1);
10316        assert_eq!(logged_metrics[0].event_codes, fidl_connect_event_code);
10317        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(3));
10318
10319        // Check that selected candidate score is logged
10320        let logged_metrics = test_helper.get_logged_metrics(metrics::SELECTED_BSS_SCORE_METRIC_ID);
10321        assert_eq!(logged_metrics.len(), 1);
10322        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(70));
10323
10324        // Check that runner-up score delta is logged
10325        let logged_metrics =
10326            test_helper.get_logged_metrics(metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID);
10327        assert_eq!(logged_metrics.len(), 1);
10328        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(10));
10329
10330        // Check that GHz score delta is logged
10331        let logged_metrics =
10332            test_helper.get_logged_metrics(metrics::BEST_CANDIDATES_GHZ_SCORE_DELTA_METRIC_ID);
10333        assert_eq!(logged_metrics.len(), 1);
10334        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(-20));
10335
10336        // Check that GHz bands present in selection is logged
10337        let logged_metrics =
10338            test_helper.get_logged_metrics(metrics::GHZ_BANDS_AVAILABLE_IN_BSS_SELECTION_METRIC_ID);
10339        assert_eq!(logged_metrics.len(), 1);
10340        assert_eq!(
10341            logged_metrics[0].event_codes,
10342            vec![metrics::GhzBandsAvailableInBssSelectionMetricDimensionBands::MultiBand as u32]
10343        );
10344        assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
10345    }
10346
10347    #[fuchsia::test]
10348    fn test_log_bss_selection_metrics_none_selected() {
10349        let (mut test_helper, mut test_fut) = setup_test();
10350
10351        test_helper.telemetry_sender.send(TelemetryEvent::BssSelectionResult {
10352            reason: client::types::ConnectReason::FidlConnectRequest,
10353            scored_candidates: vec![],
10354            selected_candidate: None,
10355        });
10356
10357        test_helper.drain_cobalt_events(&mut test_fut);
10358
10359        // Check that only the BSS selection occurrence and candidate count metrics are recorded
10360        assert!(!test_helper.get_logged_metrics(metrics::BSS_SELECTION_COUNT_METRIC_ID).is_empty());
10361        assert!(
10362            !test_helper
10363                .get_logged_metrics(metrics::BSS_SELECTION_COUNT_DETAILED_METRIC_ID)
10364                .is_empty()
10365        );
10366        assert!(
10367            !test_helper
10368                .get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_METRIC_ID)
10369                .is_empty()
10370        );
10371        assert!(
10372            !test_helper
10373                .get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_DETAILED_METRIC_ID)
10374                .is_empty()
10375        );
10376        assert!(test_helper.get_logged_metrics(metrics::BSS_CANDIDATE_SCORE_METRIC_ID).is_empty());
10377        assert!(
10378            test_helper
10379                .get_logged_metrics(metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID)
10380                .is_empty()
10381        );
10382        assert!(
10383            test_helper
10384                .get_logged_metrics(metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID)
10385                .is_empty()
10386        );
10387        assert!(
10388            test_helper
10389                .get_logged_metrics(metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID)
10390                .is_empty()
10391        );
10392        assert!(
10393            test_helper
10394                .get_logged_metrics(metrics::BEST_CANDIDATES_GHZ_SCORE_DELTA_METRIC_ID)
10395                .is_empty()
10396        );
10397        assert!(
10398            test_helper
10399                .get_logged_metrics(metrics::GHZ_BANDS_AVAILABLE_IN_BSS_SELECTION_METRIC_ID)
10400                .is_empty()
10401        );
10402    }
10403
10404    #[fuchsia::test]
10405    fn test_log_bss_selection_metrics_runner_up_delta_not_recorded() {
10406        let (mut test_helper, mut test_fut) = setup_test();
10407
10408        let scored_candidates = vec![
10409            (generate_random_scanned_candidate(), 90),
10410            (generate_random_scanned_candidate(), 60),
10411            (generate_random_scanned_candidate(), 50),
10412        ];
10413
10414        test_helper.telemetry_sender.send(TelemetryEvent::BssSelectionResult {
10415            reason: client::types::ConnectReason::FidlConnectRequest,
10416            scored_candidates,
10417            // Report that the selected candidate was not the highest scoring candidate.
10418            selected_candidate: Some((generate_random_scanned_candidate(), 60)),
10419        });
10420
10421        test_helper.drain_cobalt_events(&mut test_fut);
10422
10423        // No delta metric should be recorded
10424        assert!(
10425            test_helper
10426                .get_logged_metrics(metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID)
10427                .is_empty()
10428        );
10429    }
10430
10431    #[fuchsia::test]
10432    fn test_log_connection_score_average_long_duration() {
10433        let (mut test_helper, mut test_fut) = setup_test();
10434        let now = fasync::MonotonicInstant::now();
10435        let signals = vec![
10436            client::types::TimestampedSignal {
10437                signal: client::types::Signal { rssi_dbm: -60, snr_db: 30 },
10438                time: now,
10439            },
10440            client::types::TimestampedSignal {
10441                signal: client::types::Signal { rssi_dbm: -60, snr_db: 30 },
10442                time: now,
10443            },
10444            client::types::TimestampedSignal {
10445                signal: client::types::Signal { rssi_dbm: -80, snr_db: 10 },
10446                time: now,
10447            },
10448            client::types::TimestampedSignal {
10449                signal: client::types::Signal { rssi_dbm: -80, snr_db: 10 },
10450                time: now,
10451            },
10452        ];
10453
10454        test_helper.telemetry_sender.send(TelemetryEvent::LongDurationSignals { signals });
10455        test_helper.drain_cobalt_events(&mut test_fut);
10456
10457        let logged_metrics =
10458            test_helper.get_logged_metrics(metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID);
10459        assert_eq!(logged_metrics.len(), 1);
10460        assert_eq!(
10461            logged_metrics[0].event_codes,
10462            vec![metrics::ConnectionScoreAverageMetricDimensionDuration::LongDuration as u32]
10463        );
10464        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(55));
10465
10466        // Ensure an empty score list would not cause an arithmetic error.
10467        test_helper.telemetry_sender.send(TelemetryEvent::LongDurationSignals { signals: vec![] });
10468        test_helper.drain_cobalt_events(&mut test_fut);
10469        assert_eq!(
10470            test_helper.get_logged_metrics(metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID).len(),
10471            1
10472        );
10473    }
10474
10475    #[fuchsia::test]
10476    fn test_log_connection_rssi_average_long_duration() {
10477        let (mut test_helper, mut test_fut) = setup_test();
10478        let now = fasync::MonotonicInstant::now();
10479        let signals = vec![
10480            client::types::TimestampedSignal {
10481                signal: client::types::Signal { rssi_dbm: -60, snr_db: 30 },
10482                time: now,
10483            },
10484            client::types::TimestampedSignal {
10485                signal: client::types::Signal { rssi_dbm: -60, snr_db: 30 },
10486                time: now,
10487            },
10488            client::types::TimestampedSignal {
10489                signal: client::types::Signal { rssi_dbm: -80, snr_db: 10 },
10490                time: now,
10491            },
10492            client::types::TimestampedSignal {
10493                signal: client::types::Signal { rssi_dbm: -80, snr_db: 10 },
10494                time: now,
10495            },
10496        ];
10497
10498        test_helper.telemetry_sender.send(TelemetryEvent::LongDurationSignals { signals });
10499        test_helper.drain_cobalt_events(&mut test_fut);
10500
10501        let logged_metrics =
10502            test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_AVERAGE_METRIC_ID);
10503        assert_eq!(logged_metrics.len(), 1);
10504        assert_eq!(
10505            logged_metrics[0].event_codes,
10506            vec![metrics::ConnectionScoreAverageMetricDimensionDuration::LongDuration as u32]
10507        );
10508        assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(-70));
10509
10510        // Ensure an empty score list would not cause an arithmetic error.
10511        test_helper.telemetry_sender.send(TelemetryEvent::LongDurationSignals { signals: vec![] });
10512        test_helper.drain_cobalt_events(&mut test_fut);
10513        assert_eq!(
10514            test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_AVERAGE_METRIC_ID).len(),
10515            1
10516        );
10517    }
10518
10519    #[test_case(
10520        TimeoutSource::Scan,
10521        metrics::SmeOperationTimeoutMetricDimensionStalledOperation::Scan_ ;
10522        "log scan timeout"
10523    )]
10524    #[test_case(
10525        TimeoutSource::Connect,
10526        metrics::SmeOperationTimeoutMetricDimensionStalledOperation::Connect_ ;
10527        "log connect"
10528    )]
10529    #[test_case(
10530        TimeoutSource::Disconnect,
10531        metrics::SmeOperationTimeoutMetricDimensionStalledOperation::Disconnect_ ;
10532        "log disconnect timeout"
10533    )]
10534    #[test_case(
10535        TimeoutSource::ClientStatus,
10536        metrics::SmeOperationTimeoutMetricDimensionStalledOperation::ClientStatus_ ;
10537        "log client status timeout"
10538    )]
10539    #[test_case(
10540        TimeoutSource::WmmStatus,
10541        metrics::SmeOperationTimeoutMetricDimensionStalledOperation::WmmStatus_ ;
10542        "log WMM status timeout"
10543    )]
10544    #[test_case(
10545        TimeoutSource::ApStart,
10546        metrics::SmeOperationTimeoutMetricDimensionStalledOperation::ApStart_ ;
10547        "log AP start timeout"
10548    )]
10549    #[test_case(
10550        TimeoutSource::ApStop,
10551        metrics::SmeOperationTimeoutMetricDimensionStalledOperation::ApStop_ ;
10552        "log Ap stop timeout"
10553    )]
10554    #[test_case(
10555        TimeoutSource::ApStatus,
10556        metrics::SmeOperationTimeoutMetricDimensionStalledOperation::ApStatus_ ;
10557        "log AP status timeout"
10558    )]
10559    #[test_case(
10560        TimeoutSource::GetIfaceStats,
10561        metrics::SmeOperationTimeoutMetricDimensionStalledOperation::GetCounterStats_ ;
10562        "log iface stats timeout"
10563    )]
10564    #[test_case(
10565        TimeoutSource::GetHistogramStats,
10566        metrics::SmeOperationTimeoutMetricDimensionStalledOperation::GetHistogramStats_ ;
10567        "log histogram stats timeout"
10568    )]
10569    #[fuchsia::test(add_test_attr = false)]
10570    fn test_log_sme_timeout(
10571        source: TimeoutSource,
10572        expected_dimension: metrics::SmeOperationTimeoutMetricDimensionStalledOperation,
10573    ) {
10574        let (mut test_helper, mut test_fut) = setup_test();
10575
10576        // Send the timeout event
10577        test_helper.telemetry_sender.send(TelemetryEvent::SmeTimeout { source });
10578
10579        // Run the telemetry loop until it stalls.
10580        assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
10581
10582        // Expect that Cobalt has been notified of the timeout
10583        assert_matches!(
10584            test_helper.exec.run_until_stalled(&mut test_helper.cobalt_stream.next()),
10585            Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerRequest::LogOccurrence {
10586                metric_id, event_codes, responder, ..
10587            }))) => {
10588                assert_eq!(metric_id, metrics::SME_OPERATION_TIMEOUT_METRIC_ID);
10589                assert_eq!(event_codes, vec![expected_dimension.as_event_code()]);
10590
10591                assert!(responder.send(Ok(())).is_ok());
10592        });
10593    }
10594
10595    struct TestHelper {
10596        telemetry_sender: TelemetrySender,
10597        inspector: Inspector,
10598        monitor_svc_stream: fidl_fuchsia_wlan_device_service::DeviceMonitorRequestStream,
10599        telemetry_svc_stream: Option<fidl_fuchsia_wlan_sme::TelemetryRequestStream>,
10600        cobalt_stream: fidl_fuchsia_metrics::MetricEventLoggerRequestStream,
10601        iface_stats_resp:
10602            Option<Box<dyn Fn() -> fidl_fuchsia_wlan_sme::TelemetryGetIfaceStatsResult>>,
10603        /// As requests to Cobalt are responded to via `self.drain_cobalt_events()`,
10604        /// their payloads are drained to this HashMap
10605        cobalt_events: Vec<MetricEvent>,
10606        _defect_receiver: mpsc::Receiver<Defect>,
10607
10608        // Note: keep the executor field last in the struct so it gets dropped last.
10609        exec: fasync::TestExecutor,
10610    }
10611
10612    impl TestHelper {
10613        /// Advance executor until stalled.
10614        /// This function will also reply to any ongoing requests to establish an iface
10615        /// telemetry channel.
10616        fn advance_test_fut<T>(
10617            &mut self,
10618            test_fut: &mut (impl Future<Output = T> + Unpin),
10619        ) -> Poll<T> {
10620            let result = self.exec.run_until_stalled(test_fut);
10621            if let Poll::Ready(Some(Ok(req))) =
10622                self.exec.run_until_stalled(&mut self.monitor_svc_stream.next())
10623            {
10624                match req {
10625                    fidl_fuchsia_wlan_device_service::DeviceMonitorRequest::GetSmeTelemetry {
10626                        iface_id,
10627                        telemetry_server,
10628                        responder,
10629                    } => {
10630                        assert_eq!(iface_id, IFACE_ID);
10631                        let telemetry_stream = telemetry_server.into_stream();
10632                        responder.send(Ok(())).expect("Failed to respond to telemetry request");
10633                        self.telemetry_svc_stream = Some(telemetry_stream);
10634                        self.exec.run_until_stalled(test_fut)
10635                    }
10636                    _ => panic!("Unexpected device monitor request: {req:?}"),
10637                }
10638            } else {
10639                result
10640            }
10641        }
10642
10643        /// Advance executor by `duration`.
10644        /// This function repeatedly advances the executor by 1 second, triggering
10645        /// any expired timers and 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            assert_eq!(
10652                duration.into_nanos() % STEP_INCREMENT.into_nanos(),
10653                0,
10654                "duration {duration:?} is not divisible by STEP_INCREMENT",
10655            );
10656            const_assert_eq!(
10657                TELEMETRY_QUERY_INTERVAL.into_nanos() % STEP_INCREMENT.into_nanos(),
10658                0
10659            );
10660
10661            for _i in 0..(duration.into_nanos() / STEP_INCREMENT.into_nanos()) {
10662                self.exec.set_fake_time(fasync::MonotonicInstant::after(STEP_INCREMENT));
10663                let _ = self.exec.wake_expired_timers();
10664                assert_eq!(self.advance_test_fut(&mut test_fut), Poll::Pending);
10665
10666                if let Some(telemetry_svc_stream) = &mut self.telemetry_svc_stream
10667                    && !telemetry_svc_stream.is_terminated()
10668                {
10669                    respond_iface_counter_stats_req(
10670                        &mut self.exec,
10671                        telemetry_svc_stream,
10672                        &self.iface_stats_resp,
10673                    );
10674                }
10675
10676                // Respond to any potential Cobalt request, draining their payloads to
10677                // `self.cobalt_events`.
10678                self.drain_cobalt_events(&mut test_fut);
10679
10680                assert_eq!(self.advance_test_fut(&mut test_fut), Poll::Pending);
10681            }
10682        }
10683
10684        fn set_iface_stats_resp(
10685            &mut self,
10686            iface_stats_resp: Box<dyn Fn() -> fidl_fuchsia_wlan_sme::TelemetryGetIfaceStatsResult>,
10687        ) {
10688            let _ = self.iface_stats_resp.replace(iface_stats_resp);
10689        }
10690
10691        /// Advance executor by some duration until the next time `test_fut` handles periodic
10692        /// telemetry. This uses `self.advance_by` underneath.
10693        ///
10694        /// This function assumes that executor starts test_fut at time 0 (which should be true
10695        /// if TestHelper is created from `setup_test()`)
10696        fn advance_to_next_telemetry_checkpoint(
10697            &mut self,
10698            test_fut: Pin<&mut impl Future<Output = ()>>,
10699        ) {
10700            let now = fasync::MonotonicInstant::now();
10701            let remaining_interval = TELEMETRY_QUERY_INTERVAL.into_nanos()
10702                - (now.into_nanos() % TELEMETRY_QUERY_INTERVAL.into_nanos());
10703            self.advance_by(zx::MonotonicDuration::from_nanos(remaining_interval), test_fut)
10704        }
10705
10706        /// Continually execute the future and respond to any incoming Cobalt request with Ok.
10707        /// Append each metric request payload into `self.cobalt_events`.
10708        fn drain_cobalt_events(&mut self, test_fut: &mut (impl Future + Unpin)) {
10709            let mut made_progress = true;
10710            while made_progress {
10711                let _result = self.advance_test_fut(test_fut);
10712                made_progress = false;
10713                while let Poll::Ready(Some(Ok(req))) =
10714                    self.exec.run_until_stalled(&mut self.cobalt_stream.next())
10715                {
10716                    self.cobalt_events.append(&mut req.respond_to_metric_req(Ok(())));
10717                    made_progress = true;
10718                }
10719            }
10720        }
10721
10722        fn get_logged_metrics(&self, metric_id: u32) -> Vec<MetricEvent> {
10723            self.cobalt_events.iter().filter(|ev| ev.metric_id == metric_id).cloned().collect()
10724        }
10725
10726        fn send_connected_event(&mut self, ap_state: impl Into<client::types::ApState>) {
10727            let event = TelemetryEvent::ConnectResult {
10728                iface_id: IFACE_ID,
10729                policy_connect_reason: Some(
10730                    client::types::ConnectReason::RetryAfterFailedConnectAttempt,
10731                ),
10732                result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
10733                multiple_bss_candidates: true,
10734                ap_state: ap_state.into(),
10735                network_is_likely_hidden: true,
10736            };
10737            self.telemetry_sender.send(event);
10738        }
10739
10740        // Empty the cobalt metrics can be stored so that future checks on cobalt metrics can
10741        // ignore previous values.
10742        fn clear_cobalt_events(&mut self) {
10743            self.cobalt_events = Vec::new();
10744        }
10745
10746        fn get_time_series(
10747            &mut self,
10748            test_fut: &mut (impl Future<Output = ()> + Unpin),
10749        ) -> Arc<Mutex<TimeSeriesStats>> {
10750            let (sender, mut receiver) = oneshot::channel();
10751            self.telemetry_sender.send(TelemetryEvent::GetTimeSeries { sender });
10752            assert_matches!(self.advance_test_fut(test_fut), Poll::Pending);
10753            self.drain_cobalt_events(test_fut);
10754            assert_matches!(receiver.try_recv(), Ok(Some(stats)) => stats)
10755        }
10756    }
10757
10758    fn respond_iface_counter_stats_req(
10759        executor: &mut fasync::TestExecutor,
10760        telemetry_svc_stream: &mut fidl_fuchsia_wlan_sme::TelemetryRequestStream,
10761        iface_stats_resp: &Option<
10762            Box<dyn Fn() -> fidl_fuchsia_wlan_sme::TelemetryGetIfaceStatsResult>,
10763        >,
10764    ) {
10765        let telemetry_svc_req_fut = telemetry_svc_stream.try_next();
10766        let mut telemetry_svc_req_fut = pin!(telemetry_svc_req_fut);
10767        if let Poll::Ready(Ok(Some(request))) =
10768            executor.run_until_stalled(&mut telemetry_svc_req_fut)
10769        {
10770            match request {
10771                fidl_fuchsia_wlan_sme::TelemetryRequest::GetIfaceStats { responder } => {
10772                    let resp = match &iface_stats_resp {
10773                        Some(get_resp) => get_resp(),
10774                        None => {
10775                            let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
10776                            Ok(fidl_fuchsia_wlan_stats::IfaceStats {
10777                                connection_stats: Some(fake_connection_stats(seed)),
10778                                ..Default::default()
10779                            })
10780                        }
10781                    };
10782                    responder
10783                        .send(resp.as_ref().map_err(|e| *e))
10784                        .expect("expect sending GetIfaceStats response to succeed");
10785                }
10786                _ => {
10787                    panic!("unexpected request: {request:?}");
10788                }
10789            }
10790        }
10791    }
10792
10793    fn respond_iface_histogram_stats_req(
10794        executor: &mut fasync::TestExecutor,
10795        telemetry_svc_stream: &mut fidl_fuchsia_wlan_sme::TelemetryRequestStream,
10796    ) {
10797        let telemetry_svc_req_fut = telemetry_svc_stream.try_next();
10798        let mut telemetry_svc_req_fut = pin!(telemetry_svc_req_fut);
10799        if let Poll::Ready(Ok(Some(request))) =
10800            executor.run_until_stalled(&mut telemetry_svc_req_fut)
10801        {
10802            match request {
10803                fidl_fuchsia_wlan_sme::TelemetryRequest::GetHistogramStats { responder } => {
10804                    responder
10805                        .send(Ok(&fake_iface_histogram_stats()))
10806                        .expect("expect sending GetHistogramStats response to succeed");
10807                }
10808                _ => {
10809                    panic!("unexpected request: {request:?}");
10810                }
10811            }
10812        }
10813    }
10814
10815    /// Assert two set of Cobalt MetricEvent equal, disregarding the order
10816    #[track_caller]
10817    fn assert_eq_cobalt_events(
10818        mut left: Vec<fidl_fuchsia_metrics::MetricEvent>,
10819        mut right: Vec<fidl_fuchsia_metrics::MetricEvent>,
10820    ) {
10821        left.sort_by(metric_event_cmp);
10822        right.sort_by(metric_event_cmp);
10823        assert_eq!(left, right);
10824    }
10825
10826    fn metric_event_cmp(
10827        left: &fidl_fuchsia_metrics::MetricEvent,
10828        right: &fidl_fuchsia_metrics::MetricEvent,
10829    ) -> std::cmp::Ordering {
10830        match left.metric_id.cmp(&right.metric_id) {
10831            std::cmp::Ordering::Equal => match left.event_codes.len().cmp(&right.event_codes.len())
10832            {
10833                std::cmp::Ordering::Equal => (),
10834                ordering => return ordering,
10835            },
10836            ordering => return ordering,
10837        }
10838
10839        for i in 0..left.event_codes.len() {
10840            match left.event_codes[i].cmp(&right.event_codes[i]) {
10841                std::cmp::Ordering::Equal => (),
10842                ordering => return ordering,
10843            }
10844        }
10845
10846        match (&left.payload, &right.payload) {
10847            (MetricEventPayload::Count(v1), MetricEventPayload::Count(v2)) => v1.cmp(v2),
10848            (MetricEventPayload::IntegerValue(v1), MetricEventPayload::IntegerValue(v2)) => {
10849                v1.cmp(v2)
10850            }
10851            (MetricEventPayload::StringValue(v1), MetricEventPayload::StringValue(v2)) => {
10852                v1.cmp(v2)
10853            }
10854            (MetricEventPayload::Histogram(_), MetricEventPayload::Histogram(_)) => {
10855                unimplemented!()
10856            }
10857            _ => unimplemented!(),
10858        }
10859    }
10860
10861    trait CobaltExt {
10862        // Respond to MetricEventLoggerRequest and extract its MetricEvent
10863        fn respond_to_metric_req(
10864            self,
10865            result: Result<(), fidl_fuchsia_metrics::Error>,
10866        ) -> Vec<fidl_fuchsia_metrics::MetricEvent>;
10867    }
10868
10869    impl CobaltExt for MetricEventLoggerRequest {
10870        fn respond_to_metric_req(
10871            self,
10872            result: Result<(), fidl_fuchsia_metrics::Error>,
10873        ) -> Vec<fidl_fuchsia_metrics::MetricEvent> {
10874            match self {
10875                Self::LogOccurrence { metric_id, count, event_codes, responder } => {
10876                    assert!(responder.send(result).is_ok());
10877                    vec![MetricEvent {
10878                        metric_id,
10879                        event_codes,
10880                        payload: MetricEventPayload::Count(count),
10881                    }]
10882                }
10883                Self::LogInteger { metric_id, value, event_codes, responder } => {
10884                    assert!(responder.send(result).is_ok());
10885                    vec![MetricEvent {
10886                        metric_id,
10887                        event_codes,
10888                        payload: MetricEventPayload::IntegerValue(value),
10889                    }]
10890                }
10891                Self::LogIntegerHistogram { metric_id, histogram, event_codes, responder } => {
10892                    assert!(responder.send(result).is_ok());
10893                    vec![MetricEvent {
10894                        metric_id,
10895                        event_codes,
10896                        payload: MetricEventPayload::Histogram(histogram),
10897                    }]
10898                }
10899                Self::LogString { metric_id, string_value, event_codes, responder } => {
10900                    assert!(responder.send(result).is_ok());
10901                    vec![MetricEvent {
10902                        metric_id,
10903                        event_codes,
10904                        payload: MetricEventPayload::StringValue(string_value),
10905                    }]
10906                }
10907                Self::LogMetricEvents { events, responder } => {
10908                    assert!(responder.send(result).is_ok());
10909                    events
10910                }
10911            }
10912        }
10913    }
10914
10915    fn setup_test() -> (TestHelper, Pin<Box<impl Future<Output = ()>>>) {
10916        let mut exec = fasync::TestExecutor::new_with_fake_time();
10917        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
10918
10919        let (monitor_svc_proxy, monitor_svc_stream) =
10920            create_proxy_and_stream::<fidl_fuchsia_wlan_device_service::DeviceMonitorMarker>();
10921
10922        let (cobalt_proxy, cobalt_stream) =
10923            create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
10924
10925        let inspector = Inspector::default();
10926        let inspect_node = inspector.root().create_child("stats");
10927        let external_inspect_node = inspector.root().create_child("external");
10928        let (defect_sender, _defect_receiver) = mpsc::channel(100);
10929        let (telemetry_sender, test_fut) = serve_telemetry(
10930            monitor_svc_proxy,
10931            cobalt_proxy.clone(),
10932            inspect_node,
10933            external_inspect_node.create_child("stats"),
10934            defect_sender,
10935        );
10936        inspector.root().record(external_inspect_node);
10937        let mut test_fut = Box::pin(test_fut);
10938
10939        assert_eq!(exec.run_until_stalled(&mut test_fut), Poll::Pending);
10940
10941        let test_helper = TestHelper {
10942            telemetry_sender,
10943            inspector,
10944            monitor_svc_stream,
10945            telemetry_svc_stream: None,
10946            cobalt_stream,
10947            iface_stats_resp: None,
10948            cobalt_events: vec![],
10949            _defect_receiver,
10950            exec,
10951        };
10952        (test_helper, test_fut)
10953    }
10954
10955    fn fake_connection_stats(nth_req: u64) -> fidl_fuchsia_wlan_stats::ConnectionStats {
10956        fidl_fuchsia_wlan_stats::ConnectionStats {
10957            connection_id: Some(1),
10958            rx_unicast_total: Some(nth_req),
10959            rx_unicast_drop: Some(0),
10960            rx_multicast: Some(2 * nth_req),
10961            tx_total: Some(nth_req),
10962            tx_drop: Some(0),
10963            ..Default::default()
10964        }
10965    }
10966
10967    fn fake_iface_histogram_stats() -> fidl_fuchsia_wlan_stats::IfaceHistogramStats {
10968        fidl_fuchsia_wlan_stats::IfaceHistogramStats {
10969            noise_floor_histograms: Some(fake_noise_floor_histograms()),
10970            rssi_histograms: Some(fake_rssi_histograms()),
10971            rx_rate_index_histograms: Some(fake_rx_rate_index_histograms()),
10972            snr_histograms: Some(fake_snr_histograms()),
10973            ..Default::default()
10974        }
10975    }
10976
10977    fn fake_noise_floor_histograms() -> Vec<fidl_fuchsia_wlan_stats::NoiseFloorHistogram> {
10978        vec![fidl_fuchsia_wlan_stats::NoiseFloorHistogram {
10979            hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
10980            antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
10981                freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G,
10982                index: 0,
10983            })),
10984            noise_floor_samples: vec![
10985                // We normally don't expect the driver to send buckets with zero samples, but
10986                // mock them here anyway so we can test that we filter them out if they exist.
10987                fidl_fuchsia_wlan_stats::HistBucket { bucket_index: 199, num_samples: 0 },
10988                fidl_fuchsia_wlan_stats::HistBucket { bucket_index: 200, num_samples: 999 },
10989            ],
10990            invalid_samples: 44,
10991        }]
10992    }
10993
10994    fn fake_rssi_histograms() -> Vec<fidl_fuchsia_wlan_stats::RssiHistogram> {
10995        vec![fidl_fuchsia_wlan_stats::RssiHistogram {
10996            hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
10997            antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
10998                freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G,
10999                index: 0,
11000            })),
11001            rssi_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
11002                bucket_index: 230,
11003                num_samples: 999,
11004            }],
11005            invalid_samples: 55,
11006        }]
11007    }
11008
11009    fn fake_rx_rate_index_histograms() -> Vec<fidl_fuchsia_wlan_stats::RxRateIndexHistogram> {
11010        vec![
11011            fidl_fuchsia_wlan_stats::RxRateIndexHistogram {
11012                hist_scope: fidl_fuchsia_wlan_stats::HistScope::Station,
11013                antenna_id: None,
11014                rx_rate_index_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
11015                    bucket_index: 99,
11016                    num_samples: 1400,
11017                }],
11018                invalid_samples: 22,
11019            },
11020            fidl_fuchsia_wlan_stats::RxRateIndexHistogram {
11021                hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
11022                antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
11023                    freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna5G,
11024                    index: 1,
11025                })),
11026                rx_rate_index_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
11027                    bucket_index: 100,
11028                    num_samples: 1500,
11029                }],
11030                invalid_samples: 33,
11031            },
11032        ]
11033    }
11034
11035    fn fake_snr_histograms() -> Vec<fidl_fuchsia_wlan_stats::SnrHistogram> {
11036        vec![fidl_fuchsia_wlan_stats::SnrHistogram {
11037            hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
11038            antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
11039                freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G,
11040                index: 0,
11041            })),
11042            snr_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
11043                bucket_index: 30,
11044                num_samples: 999,
11045            }],
11046            invalid_samples: 11,
11047        }]
11048    }
11049
11050    fn fake_disconnect_info() -> DisconnectInfo {
11051        let is_sme_reconnecting = false;
11052        let fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting);
11053        DisconnectInfo {
11054            connected_duration: zx::MonotonicDuration::from_hours(6),
11055            is_sme_reconnecting: fidl_disconnect_info.is_sme_reconnecting,
11056            disconnect_source: fidl_disconnect_info.disconnect_source,
11057            previous_connect_reason: client::types::ConnectReason::IdleInterfaceAutoconnect,
11058            ap_state: random_bss_description!(Wpa2).into(),
11059            signals: HistoricalList::new(8),
11060        }
11061    }
11062
11063    fn fake_connect_result(code: fidl_ieee80211::StatusCode) -> fidl_sme::ConnectResult {
11064        fidl_sme::ConnectResult { code, is_credential_rejected: false, is_reconnect: false }
11065    }
11066
11067    #[fuchsia::test]
11068    fn test_error_throttling() {
11069        let exec = fasync::TestExecutor::new_with_fake_time();
11070        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
11071        let mut error_logger = ThrottledErrorLogger::new(MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS);
11072
11073        // Set the fake time to 61 minutes past 0 time to ensure that messages will be logged.
11074        exec.set_fake_time(fasync::MonotonicInstant::after(
11075            fasync::MonotonicDuration::from_minutes(MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS + 1),
11076        ));
11077
11078        // Log an error and verify that no record of it was retained (ie: the error was emitted
11079        // immediately).
11080        error_logger.throttle_error(Err(format_err!("")));
11081        assert!(!error_logger.suppressed_errors.contains_key(&String::from("")));
11082
11083        // Log another error and verify that the error counter has been incremented.
11084        error_logger.throttle_error(Err(format_err!("")));
11085        assert_eq!(error_logger.suppressed_errors[&String::from("")], 1);
11086
11087        // Advance time again and log another error to verify that the counter resets (ie: log was
11088        // emitted).
11089        exec.set_fake_time(fasync::MonotonicInstant::after(
11090            fasync::MonotonicDuration::from_minutes(MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS + 1),
11091        ));
11092        error_logger.throttle_error(Err(format_err!("")));
11093        assert!(!error_logger.suppressed_errors.contains_key(&String::from("")));
11094
11095        // Log another error to verify that the counter begins incrementing again.
11096        error_logger.throttle_error(Err(format_err!("")));
11097        assert_eq!(error_logger.suppressed_errors[&String::from("")], 1);
11098    }
11099}