Skip to main content

wlancfg_lib/telemetry/
mod.rs

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