wlancfg_lib/telemetry/
mod.rs

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