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