Skip to main content

wlan_telemetry/processors/
connect_disconnect.rs

1// Copyright 2024 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
5use crate::processors::toggle_events::ClientConnectionsToggleEvent;
6use crate::util::cobalt_logger::log_cobalt_batch;
7use derivative::Derivative;
8use fidl_fuchsia_metrics::{MetricEvent, MetricEventPayload};
9use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
10use fidl_fuchsia_wlan_sme as fidl_sme;
11use fuchsia_async as fasync;
12use fuchsia_inspect::Node as InspectNode;
13use fuchsia_inspect_contrib::id_enum::IdEnum;
14use fuchsia_inspect_contrib::inspect_log;
15use fuchsia_inspect_contrib::nodes::{BoundedListNode, LruCacheNode};
16use fuchsia_inspect_derive::Unit;
17use fuchsia_sync::Mutex;
18use std::sync::Arc;
19use std::sync::atomic::{AtomicUsize, Ordering};
20use strum_macros::{Display, EnumIter};
21use windowed_stats::experimental::inspect::{InspectSender, InspectedTimeMatrix};
22use windowed_stats::experimental::series::interpolation::{ConstantSample, LastSample};
23use windowed_stats::experimental::series::metadata::{BitSetMap, BitSetNode};
24use windowed_stats::experimental::series::statistic::Union;
25use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
26use wlan_common::bss::BssDescription;
27use wlan_common::channel::Channel;
28use wlan_legacy_metrics_registry as metrics;
29use zx;
30
31const INSPECT_CONNECT_EVENTS_LIMIT: usize = 10;
32const INSPECT_DISCONNECT_EVENTS_LIMIT: usize = 20;
33const INSPECT_CONNECT_ATTEMPT_RESULTS_LIMIT: usize = 50;
34const INSPECT_CONNECTED_NETWORKS_ID_LIMIT: usize = 16;
35const INSPECT_DISCONNECT_SOURCES_ID_LIMIT: usize = 32;
36const INSPECT_CONNECT_ATTEMPT_RESULTS_ID_LIMIT: usize = 32;
37const SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT: zx::BootDuration =
38    zx::BootDuration::from_minutes(2);
39
40#[derive(Debug, Display, EnumIter)]
41enum ConnectionState {
42    Idle(IdleState),
43    Connected(ConnectedState),
44    Disconnected(DisconnectedState),
45    ConnectFailed(ConnectFailedState),
46}
47
48// Update the ConnectDisconnectTimeSeries BitSetMap when making changes to this enum.
49impl IdEnum for ConnectionState {
50    type Id = u8;
51    fn to_id(&self) -> Self::Id {
52        match self {
53            Self::Idle(_) => 0,
54            Self::Disconnected(_) => 1,
55            Self::ConnectFailed(_) => 2,
56            Self::Connected(_) => 3,
57        }
58    }
59}
60
61#[derive(Debug, Default)]
62struct IdleState {}
63
64#[derive(Debug, Default)]
65struct ConnectedState {}
66
67#[derive(Debug, Default)]
68struct DisconnectedState {}
69
70#[derive(Debug, Default)]
71struct ConnectFailedState {}
72
73#[derive(Derivative, Unit)]
74#[derivative(PartialEq, Eq, Hash)]
75struct InspectConnectedNetwork {
76    bssid: String,
77    ssid: String,
78    protection: String,
79    ht_cap: Option<Vec<u8>>,
80    vht_cap: Option<Vec<u8>>,
81    #[derivative(PartialEq = "ignore")]
82    #[derivative(Hash = "ignore")]
83    wsc: Option<InspectNetworkWsc>,
84    is_wmm_assoc: bool,
85    wmm_param: Option<Vec<u8>>,
86}
87
88impl From<&BssDescription> for InspectConnectedNetwork {
89    fn from(bss_description: &BssDescription) -> Self {
90        Self {
91            bssid: bss_description.bssid.to_string(),
92            ssid: bss_description.ssid.to_string(),
93            protection: format!("{:?}", bss_description.protection()),
94            ht_cap: bss_description.raw_ht_cap().map(|cap| cap.bytes.into()),
95            vht_cap: bss_description.raw_vht_cap().map(|cap| cap.bytes.into()),
96            wsc: bss_description.probe_resp_wsc().as_ref().map(InspectNetworkWsc::from),
97            is_wmm_assoc: bss_description.find_wmm_param().is_some(),
98            wmm_param: bss_description.find_wmm_param().map(|bytes| bytes.into()),
99        }
100    }
101}
102
103#[derive(PartialEq, Unit, Hash)]
104struct InspectNetworkWsc {
105    device_name: String,
106    manufacturer: String,
107    model_name: String,
108    model_number: String,
109}
110
111impl From<&wlan_common::ie::wsc::ProbeRespWsc> for InspectNetworkWsc {
112    fn from(wsc: &wlan_common::ie::wsc::ProbeRespWsc) -> Self {
113        Self {
114            device_name: String::from_utf8_lossy(&wsc.device_name[..]).to_string(),
115            manufacturer: String::from_utf8_lossy(&wsc.manufacturer[..]).to_string(),
116            model_name: String::from_utf8_lossy(&wsc.model_name[..]).to_string(),
117            model_number: String::from_utf8_lossy(&wsc.model_number[..]).to_string(),
118        }
119    }
120}
121
122#[derive(PartialEq, Eq, Unit, Hash)]
123struct InspectConnectAttemptResult {
124    status_code: u16,
125    result: String,
126}
127
128#[derive(PartialEq, Eq, Unit, Hash)]
129struct InspectDisconnectSource {
130    source: String,
131    reason: String,
132    mlme_event_name: Option<String>,
133}
134
135impl From<&fidl_sme::DisconnectSource> for InspectDisconnectSource {
136    fn from(disconnect_source: &fidl_sme::DisconnectSource) -> Self {
137        match disconnect_source {
138            fidl_sme::DisconnectSource::User(reason) => Self {
139                source: "user".to_string(),
140                reason: format!("{reason:?}"),
141                mlme_event_name: None,
142            },
143            fidl_sme::DisconnectSource::Ap(cause) => Self {
144                source: "ap".to_string(),
145                reason: format!("{:?}", cause.reason_code),
146                mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
147            },
148            fidl_sme::DisconnectSource::Mlme(cause) => Self {
149                source: "mlme".to_string(),
150                reason: format!("{:?}", cause.reason_code),
151                mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
152            },
153        }
154    }
155}
156
157#[derive(Clone, Debug, PartialEq)]
158pub struct DisconnectInfo {
159    pub iface_id: u16,
160    pub connected_duration: zx::BootDuration,
161    pub is_sme_reconnecting: bool,
162    pub disconnect_source: fidl_sme::DisconnectSource,
163    pub original_bss_desc: Box<BssDescription>,
164    pub current_rssi_dbm: i8,
165    pub current_snr_db: i8,
166    pub current_channel: Channel,
167}
168
169pub struct ConnectDisconnectLogger {
170    connection_state: Arc<Mutex<ConnectionState>>,
171    cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
172    connect_events_node: Mutex<BoundedListNode>,
173    disconnect_events_node: Mutex<BoundedListNode>,
174    connect_attempt_results_node: Mutex<BoundedListNode>,
175    inspect_metadata_node: Mutex<InspectMetadataNode>,
176    time_series_stats: ConnectDisconnectTimeSeries,
177    successive_connect_attempt_failures: AtomicUsize,
178    last_connect_failure_at: Arc<Mutex<Option<fasync::BootInstant>>>,
179    last_disconnect_at: Arc<Mutex<Option<fasync::MonotonicInstant>>>,
180}
181
182impl ConnectDisconnectLogger {
183    pub fn new<S: InspectSender>(
184        cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
185        inspect_node: &InspectNode,
186        inspect_metadata_node: &InspectNode,
187        inspect_metadata_path: &str,
188        time_matrix_client: &S,
189    ) -> Self {
190        let connect_events = inspect_node.create_child("connect_events");
191        let disconnect_events = inspect_node.create_child("disconnect_events");
192        let connect_attempt_results = inspect_node.create_child("connect_attempt_results");
193        let this = Self {
194            cobalt_proxy,
195            connection_state: Arc::new(Mutex::new(ConnectionState::Idle(IdleState {}))),
196            connect_events_node: Mutex::new(BoundedListNode::new(
197                connect_events,
198                INSPECT_CONNECT_EVENTS_LIMIT,
199            )),
200            disconnect_events_node: Mutex::new(BoundedListNode::new(
201                disconnect_events,
202                INSPECT_DISCONNECT_EVENTS_LIMIT,
203            )),
204            connect_attempt_results_node: Mutex::new(BoundedListNode::new(
205                connect_attempt_results,
206                INSPECT_CONNECT_ATTEMPT_RESULTS_LIMIT,
207            )),
208            inspect_metadata_node: Mutex::new(InspectMetadataNode::new(inspect_metadata_node)),
209            time_series_stats: ConnectDisconnectTimeSeries::new(
210                time_matrix_client,
211                inspect_metadata_path,
212            ),
213            successive_connect_attempt_failures: AtomicUsize::new(0),
214            last_connect_failure_at: Arc::new(Mutex::new(None)),
215            last_disconnect_at: Arc::new(Mutex::new(None)),
216        };
217        this.log_connection_state();
218        this
219    }
220
221    fn update_connection_state(&self, state: ConnectionState) {
222        *self.connection_state.lock() = state;
223        self.log_connection_state();
224    }
225
226    fn log_connection_state(&self) {
227        let wlan_connectivity_state_id = self.connection_state.lock().to_id() as u64;
228        self.time_series_stats.log_wlan_connectivity_state(1 << wlan_connectivity_state_id);
229    }
230
231    pub async fn handle_connect_attempt(
232        &self,
233        result: fidl_ieee80211::StatusCode,
234        bss: &BssDescription,
235        is_credential_rejected: bool,
236    ) {
237        let mut flushed_successive_failures = None;
238        let mut downtime_duration = None;
239        if result == fidl_ieee80211::StatusCode::Success {
240            self.update_connection_state(ConnectionState::Connected(ConnectedState {}));
241            flushed_successive_failures =
242                Some(self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst));
243            downtime_duration =
244                self.last_disconnect_at.lock().map(|t| fasync::MonotonicInstant::now() - t);
245        } else if is_credential_rejected {
246            self.update_connection_state(ConnectionState::Idle(IdleState {}));
247            let _prev = self.successive_connect_attempt_failures.fetch_add(1, Ordering::SeqCst);
248            let _prev = self.last_connect_failure_at.lock().replace(fasync::BootInstant::now());
249        } else {
250            self.update_connection_state(ConnectionState::ConnectFailed(ConnectFailedState {}));
251            let _prev = self.successive_connect_attempt_failures.fetch_add(1, Ordering::SeqCst);
252            let _prev = self.last_connect_failure_at.lock().replace(fasync::BootInstant::now());
253        }
254
255        self.log_connect_attempt_inspect(result, bss);
256        self.log_connect_attempt_cobalt(result, flushed_successive_failures, downtime_duration)
257            .await;
258    }
259
260    fn log_connect_attempt_inspect(
261        &self,
262        result: fidl_ieee80211::StatusCode,
263        bss: &BssDescription,
264    ) {
265        let mut inspect_metadata_node = self.inspect_metadata_node.lock();
266        let connect_result_id =
267            inspect_metadata_node.connect_attempt_results.insert(InspectConnectAttemptResult {
268                status_code: result.into_primitive(),
269                result: format!("{:?}", result),
270            }) as u64;
271        self.time_series_stats.log_connect_attempt_results(1 << connect_result_id);
272
273        inspect_log!(self.connect_attempt_results_node.lock(), {
274            result: format!("{:?}", result),
275            ssid: bss.ssid.to_string(),
276            bssid: bss.bssid.to_string(),
277            protection: format!("{:?}", bss.protection()),
278        });
279
280        if result == fidl_ieee80211::StatusCode::Success {
281            let connected_network = InspectConnectedNetwork::from(bss);
282            let connected_network_id =
283                inspect_metadata_node.connected_networks.insert(connected_network) as u64;
284
285            self.time_series_stats.log_connected_networks(1 << connected_network_id);
286
287            inspect_log!(self.connect_events_node.lock(), {
288                network_id: connected_network_id,
289            });
290        }
291    }
292
293    #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
294    async fn log_connect_attempt_cobalt(
295        &self,
296        result: fidl_ieee80211::StatusCode,
297        flushed_successive_failures: Option<usize>,
298        downtime_duration: Option<zx::MonotonicDuration>,
299    ) {
300        let mut metric_events = vec![];
301        metric_events.push(MetricEvent {
302            metric_id: metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
303            event_codes: vec![result.into_primitive() as u32],
304            payload: MetricEventPayload::Count(1),
305        });
306
307        if let Some(failures) = flushed_successive_failures {
308            metric_events.push(MetricEvent {
309                metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
310                event_codes: vec![],
311                payload: MetricEventPayload::IntegerValue(failures as i64),
312            });
313        }
314
315        if let Some(duration) = downtime_duration {
316            metric_events.push(MetricEvent {
317                metric_id: metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID,
318                event_codes: vec![],
319                payload: MetricEventPayload::IntegerValue(duration.into_millis()),
320            });
321        }
322
323        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_connect_attempt_cobalt");
324    }
325
326    pub async fn log_disconnect(&self, info: &DisconnectInfo) {
327        // Mobile devices can be considered idle if they disconnect for reasons associated with
328        // going out of range or are commanded to disconnect by upper layers.
329        //
330        // TODO(500107852): Update this logic to account for non-mobile devices when such devices
331        // use the telemetry library.
332        if !info.disconnect_source.should_log_for_mobile_device() {
333            self.update_connection_state(ConnectionState::Idle(IdleState {}));
334        } else {
335            self.update_connection_state(ConnectionState::Disconnected(DisconnectedState {}));
336        }
337        let _prev = self.last_disconnect_at.lock().replace(fasync::MonotonicInstant::now());
338        self.log_disconnect_inspect(info);
339        self.log_disconnect_cobalt(info).await;
340    }
341
342    fn log_disconnect_inspect(&self, info: &DisconnectInfo) {
343        let mut inspect_metadata_node = self.inspect_metadata_node.lock();
344        let connected_network = InspectConnectedNetwork::from(&*info.original_bss_desc);
345        let connected_network_id =
346            inspect_metadata_node.connected_networks.insert(connected_network) as u64;
347        let disconnect_source = InspectDisconnectSource::from(&info.disconnect_source);
348        let disconnect_source_id =
349            inspect_metadata_node.disconnect_sources.insert(disconnect_source) as u64;
350        inspect_log!(self.disconnect_events_node.lock(), {
351            connected_duration: info.connected_duration.into_nanos(),
352            disconnect_source_id: disconnect_source_id,
353            network_id: connected_network_id,
354            rssi_dbm: info.current_rssi_dbm,
355            snr_db: info.current_snr_db,
356            channel: format!("{}", info.current_channel),
357        });
358
359        self.time_series_stats.log_disconnected_networks(1 << connected_network_id);
360        self.time_series_stats.log_disconnect_sources(1 << disconnect_source_id);
361    }
362
363    async fn log_disconnect_cobalt(&self, info: &DisconnectInfo) {
364        let mut metric_events = vec![];
365        metric_events.push(MetricEvent {
366            metric_id: metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID,
367            event_codes: vec![],
368            payload: MetricEventPayload::Count(1),
369        });
370
371        if info.disconnect_source.should_log_for_mobile_device() {
372            metric_events.push(MetricEvent {
373                metric_id: metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID,
374                event_codes: vec![],
375                payload: MetricEventPayload::Count(1),
376            });
377        }
378
379        metric_events.push(MetricEvent {
380            metric_id: metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID,
381            event_codes: vec![],
382            payload: MetricEventPayload::IntegerValue(info.connected_duration.into_millis()),
383        });
384
385        metric_events.push(MetricEvent {
386            metric_id: metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID,
387            event_codes: vec![
388                u32::from(info.disconnect_source.cobalt_reason_code()),
389                info.disconnect_source.as_cobalt_disconnect_source() as u32,
390            ],
391            payload: MetricEventPayload::Count(1),
392        });
393
394        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_disconnect_cobalt");
395    }
396
397    pub async fn handle_periodic_telemetry(&self) {
398        let mut metric_events = vec![];
399        let now = fasync::BootInstant::now();
400        if let Some(failed_at) = *self.last_connect_failure_at.lock()
401            && now - failed_at >= SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT
402        {
403            let failures = self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
404            if failures > 0 {
405                metric_events.push(MetricEvent {
406                    metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
407                    event_codes: vec![],
408                    payload: MetricEventPayload::IntegerValue(failures as i64),
409                });
410            }
411        }
412
413        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_periodic_telemetry");
414    }
415
416    pub async fn handle_suspend_imminent(&self) {
417        let mut metric_events = vec![];
418
419        let flushed_successive_failures =
420            self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
421        if flushed_successive_failures > 0 {
422            metric_events.push(MetricEvent {
423                metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
424                event_codes: vec![],
425                payload: MetricEventPayload::IntegerValue(flushed_successive_failures as i64),
426            });
427        }
428
429        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_suspend_imminent");
430    }
431
432    pub async fn handle_iface_destroyed(&self) {
433        self.update_connection_state(ConnectionState::Idle(IdleState {}));
434    }
435
436    pub async fn handle_client_connections_toggle(&self, event: &ClientConnectionsToggleEvent) {
437        if event == &ClientConnectionsToggleEvent::Disabled {
438            self.update_connection_state(ConnectionState::Idle(IdleState {}));
439        }
440    }
441}
442
443struct InspectMetadataNode {
444    connected_networks: LruCacheNode<InspectConnectedNetwork>,
445    disconnect_sources: LruCacheNode<InspectDisconnectSource>,
446    connect_attempt_results: LruCacheNode<InspectConnectAttemptResult>,
447}
448
449impl InspectMetadataNode {
450    const CONNECTED_NETWORKS: &'static str = "connected_networks";
451    const DISCONNECT_SOURCES: &'static str = "disconnect_sources";
452    const CONNECT_ATTEMPT_RESULTS: &'static str = "connect_attempt_results";
453
454    fn new(inspect_node: &InspectNode) -> Self {
455        let connected_networks = inspect_node.create_child(Self::CONNECTED_NETWORKS);
456        let disconnect_sources = inspect_node.create_child(Self::DISCONNECT_SOURCES);
457        let connect_attempt_results = inspect_node.create_child(Self::CONNECT_ATTEMPT_RESULTS);
458        Self {
459            connected_networks: LruCacheNode::new(
460                connected_networks,
461                INSPECT_CONNECTED_NETWORKS_ID_LIMIT,
462            ),
463            disconnect_sources: LruCacheNode::new(
464                disconnect_sources,
465                INSPECT_DISCONNECT_SOURCES_ID_LIMIT,
466            ),
467            connect_attempt_results: LruCacheNode::new(
468                connect_attempt_results,
469                INSPECT_CONNECT_ATTEMPT_RESULTS_ID_LIMIT,
470            ),
471        }
472    }
473}
474
475#[derive(Debug, Clone)]
476struct ConnectDisconnectTimeSeries {
477    wlan_connectivity_states: InspectedTimeMatrix<u64>,
478    connected_networks: InspectedTimeMatrix<u64>,
479    disconnected_networks: InspectedTimeMatrix<u64>,
480    disconnect_sources: InspectedTimeMatrix<u64>,
481    connect_attempt_results: InspectedTimeMatrix<u64>,
482}
483
484impl ConnectDisconnectTimeSeries {
485    pub fn new<S: InspectSender>(client: &S, inspect_metadata_path: &str) -> Self {
486        let wlan_connectivity_states = client.inspect_time_matrix_with_metadata(
487            "wlan_connectivity_states",
488            TimeMatrix::<Union<u64>, LastSample>::new(
489                SamplingProfile::highly_granular(),
490                LastSample::or(0),
491            ),
492            // Update the ConnectionState IdEnum trait when making changes to this list.
493            BitSetMap::from_ordered(["idle", "disconnected", "connect_failed", "connected"]),
494        );
495        let connected_networks = client.inspect_time_matrix_with_metadata(
496            "connected_networks",
497            TimeMatrix::<Union<u64>, ConstantSample>::new(
498                SamplingProfile::granular(),
499                ConstantSample::default(),
500            ),
501            BitSetNode::from_path(format!(
502                "{}/{}",
503                inspect_metadata_path,
504                InspectMetadataNode::CONNECTED_NETWORKS
505            )),
506        );
507        let disconnected_networks = client.inspect_time_matrix_with_metadata(
508            "disconnected_networks",
509            TimeMatrix::<Union<u64>, ConstantSample>::new(
510                SamplingProfile::granular(),
511                ConstantSample::default(),
512            ),
513            // This time matrix shares its bit labels with `connected_networks`.
514            BitSetNode::from_path(format!(
515                "{}/{}",
516                inspect_metadata_path,
517                InspectMetadataNode::CONNECTED_NETWORKS
518            )),
519        );
520        let disconnect_sources = client.inspect_time_matrix_with_metadata(
521            "disconnect_sources",
522            TimeMatrix::<Union<u64>, ConstantSample>::new(
523                SamplingProfile::granular(),
524                ConstantSample::default(),
525            ),
526            BitSetNode::from_path(format!(
527                "{}/{}",
528                inspect_metadata_path,
529                InspectMetadataNode::DISCONNECT_SOURCES,
530            )),
531        );
532        let connect_attempt_results = client.inspect_time_matrix_with_metadata(
533            "connect_attempt_results",
534            TimeMatrix::<Union<u64>, ConstantSample>::new(
535                SamplingProfile::granular(),
536                ConstantSample::default(),
537            ),
538            BitSetNode::from_path(format!(
539                "{}/{}",
540                inspect_metadata_path,
541                InspectMetadataNode::CONNECT_ATTEMPT_RESULTS,
542            )),
543        );
544        Self {
545            wlan_connectivity_states,
546            connected_networks,
547            disconnected_networks,
548            disconnect_sources,
549            connect_attempt_results,
550        }
551    }
552
553    fn log_wlan_connectivity_state(&self, data: u64) {
554        self.wlan_connectivity_states.fold_or_log_error(data);
555    }
556    fn log_connected_networks(&self, data: u64) {
557        self.connected_networks.fold_or_log_error(data);
558    }
559    fn log_disconnected_networks(&self, data: u64) {
560        self.disconnected_networks.fold_or_log_error(data);
561    }
562    fn log_disconnect_sources(&self, data: u64) {
563        self.disconnect_sources.fold_or_log_error(data);
564    }
565    fn log_connect_attempt_results(&self, data: u64) {
566        self.connect_attempt_results.fold_or_log_error(data);
567    }
568}
569
570pub trait DisconnectSourceExt {
571    fn should_log_for_mobile_device(&self) -> bool;
572    fn cobalt_reason_code(&self) -> u16;
573    fn as_cobalt_disconnect_source(
574        &self,
575    ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource;
576}
577
578impl DisconnectSourceExt for fidl_sme::DisconnectSource {
579    fn should_log_for_mobile_device(&self) -> bool {
580        match self {
581            fidl_sme::DisconnectSource::Ap(_) => true,
582            fidl_sme::DisconnectSource::Mlme(cause)
583                if cause.reason_code != fidl_ieee80211::ReasonCode::MlmeLinkFailed =>
584            {
585                true
586            }
587            _ => false,
588        }
589    }
590
591    fn cobalt_reason_code(&self) -> u16 {
592        let cobalt_disconnect_reason_code = match self {
593            fidl_sme::DisconnectSource::Ap(cause) | fidl_sme::DisconnectSource::Mlme(cause) => {
594                cause.reason_code.into_primitive()
595            }
596            fidl_sme::DisconnectSource::User(reason) => *reason as u16,
597        };
598        // This `max_event_code: 1000` is set in the metrics registry, but doesn't show up in the
599        // generated bindings.
600        const REASON_CODE_MAX: u16 = 1000;
601        std::cmp::min(cobalt_disconnect_reason_code, REASON_CODE_MAX)
602    }
603
604    fn as_cobalt_disconnect_source(
605        &self,
606    ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource {
607        use metrics::ConnectivityWlanMetricDimensionDisconnectSource as DS;
608        match self {
609            fidl_sme::DisconnectSource::Ap(..) => DS::Ap,
610            fidl_sme::DisconnectSource::User(..) => DS::User,
611            fidl_sme::DisconnectSource::Mlme(..) => DS::Mlme,
612        }
613    }
614}
615
616#[cfg(test)]
617mod tests {
618    use super::*;
619    use crate::testing::*;
620    use assert_matches::assert_matches;
621    use diagnostics_assertions::{
622        AnyBoolProperty, AnyBytesProperty, AnyNumericProperty, AnyStringProperty, assert_data_tree,
623    };
624
625    use futures::task::Poll;
626    use ieee80211_testutils::{BSSID_REGEX, SSID_REGEX};
627    use rand::Rng;
628    use std::pin::pin;
629    use test_case::test_case;
630    use windowed_stats::experimental::clock::Timed;
631    use windowed_stats::experimental::inspect::TimeMatrixClient;
632    use windowed_stats::experimental::testing::TimeMatrixCall;
633    use wlan_common::channel::{Cbw, Channel};
634    use wlan_common::{fake_bss_description, random_bss_description};
635
636    #[fuchsia::test]
637    fn log_connect_attempt_then_inspect_data_tree_contains_time_matrix_metadata() {
638        let mut harness = setup_test();
639
640        let client =
641            TimeMatrixClient::new(harness.inspect_node.create_child("wlan_connect_disconnect"));
642        let logger = ConnectDisconnectLogger::new(
643            harness.cobalt_proxy.clone(),
644            &harness.inspect_node,
645            &harness.inspect_metadata_node,
646            &harness.inspect_metadata_path,
647            &client,
648        );
649        let bss = random_bss_description!();
650        let mut log_connect_attempt =
651            pin!(logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss, false));
652        assert!(
653            harness.run_until_stalled_drain_cobalt_events(&mut log_connect_attempt).is_ready(),
654            "`log_connect_attempt` did not complete",
655        );
656
657        let tree = harness.get_inspect_data_tree();
658        assert_data_tree!(
659            @executor harness.exec,
660            tree,
661            root: contains {
662                test_stats: contains {
663                    wlan_connect_disconnect: contains {
664                        wlan_connectivity_states: {
665                            "type": "bitset",
666                            "data": AnyBytesProperty,
667                            metadata: {
668                                index: {
669                                    "0": "idle",
670                                    "1": "disconnected",
671                                    "2": "connect_failed",
672                                    "3": "connected",
673                                },
674                            },
675                        },
676                        connected_networks: {
677                            "type": "bitset",
678                            "data": AnyBytesProperty,
679                            metadata: {
680                                "index_node_path": "root/test_stats/metadata/connected_networks",
681                            },
682                        },
683                        disconnected_networks: {
684                            "type": "bitset",
685                            "data": AnyBytesProperty,
686                            metadata: {
687                                "index_node_path": "root/test_stats/metadata/connected_networks",
688                            },
689                        },
690                        disconnect_sources: {
691                            "type": "bitset",
692                            "data": AnyBytesProperty,
693                            metadata: {
694                                "index_node_path": "root/test_stats/metadata/disconnect_sources",
695                            },
696                        },
697                        connect_attempt_results: {
698                            "type": "bitset",
699                            "data": AnyBytesProperty,
700                            metadata: {
701                                "index_node_path": "root/test_stats/metadata/connect_attempt_results",
702                            },
703                        },
704                    },
705                },
706            }
707        );
708    }
709
710    #[fuchsia::test]
711    fn test_log_connect_attempt_inspect() {
712        let mut test_helper = setup_test();
713        let logger = ConnectDisconnectLogger::new(
714            test_helper.cobalt_proxy.clone(),
715            &test_helper.inspect_node,
716            &test_helper.inspect_metadata_node,
717            &test_helper.inspect_metadata_path,
718            &test_helper.mock_time_matrix_client,
719        );
720
721        // Log the event
722        let bss_description = random_bss_description!();
723        let mut test_fut = pin!(logger.handle_connect_attempt(
724            fidl_ieee80211::StatusCode::Success,
725            &bss_description,
726            false
727        ));
728        assert_eq!(
729            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
730            Poll::Ready(())
731        );
732
733        // Validate Inspect data
734        let data = test_helper.get_inspect_data_tree();
735        assert_data_tree!(@executor test_helper.exec, data, root: contains {
736            test_stats: contains {
737                metadata: contains {
738                    connected_networks: contains {
739                        "0": {
740                            "@time": AnyNumericProperty,
741                            "data": contains {
742                                bssid: &*BSSID_REGEX,
743                                ssid: &*SSID_REGEX,
744                            }
745                        }
746                    },
747                    connect_attempt_results: contains {
748                        "0": {
749                            "@time": AnyNumericProperty,
750                            "data": contains {
751                                status_code: 0u64,
752                                result: "Success",
753                            }
754                        }
755                    },
756                },
757                connect_events: {
758                    "0": {
759                        "@time": AnyNumericProperty,
760                        network_id: 0u64,
761                    }
762                },
763                connect_attempt_results: {
764                    "0": {
765                        "@time": AnyNumericProperty,
766                        result: "Success",
767                        ssid: &*SSID_REGEX,
768                        bssid: &*BSSID_REGEX,
769                        protection: AnyStringProperty,
770                    }
771                }
772            }
773        });
774
775        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
776        assert_eq!(
777            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
778            &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 3)),]
779        );
780        assert_eq!(
781            &time_matrix_calls.drain::<u64>("connected_networks")[..],
782            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
783        );
784        assert_eq!(
785            &time_matrix_calls.drain::<u64>("connect_attempt_results")[..],
786            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
787        );
788    }
789
790    #[fuchsia::test]
791    fn test_log_connect_attempt_cobalt() {
792        let mut test_helper = setup_test();
793        let logger = ConnectDisconnectLogger::new(
794            test_helper.cobalt_proxy.clone(),
795            &test_helper.inspect_node,
796            &test_helper.inspect_metadata_node,
797            &test_helper.inspect_metadata_path,
798            &test_helper.mock_time_matrix_client,
799        );
800
801        // Generate BSS Description
802        let bss_description = random_bss_description!(Wpa2,
803            channel: Channel::new(157, Cbw::Cbw40),
804            bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
805        );
806
807        // Log the event
808        let mut test_fut = pin!(logger.handle_connect_attempt(
809            fidl_ieee80211::StatusCode::Success,
810            &bss_description,
811            false
812        ));
813        assert_eq!(
814            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
815            Poll::Ready(())
816        );
817
818        // Validate Cobalt data
819        let breakdowns_by_status_code = test_helper
820            .get_logged_metrics(metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID);
821        assert_eq!(breakdowns_by_status_code.len(), 1);
822        assert_eq!(
823            breakdowns_by_status_code[0].event_codes,
824            vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
825        );
826        assert_eq!(breakdowns_by_status_code[0].payload, MetricEventPayload::Count(1));
827    }
828
829    #[fuchsia::test]
830    fn test_successive_connect_attempt_failures_cobalt_zero_failures() {
831        let mut test_helper = setup_test();
832        let logger = ConnectDisconnectLogger::new(
833            test_helper.cobalt_proxy.clone(),
834            &test_helper.inspect_node,
835            &test_helper.inspect_metadata_node,
836            &test_helper.inspect_metadata_path,
837            &test_helper.mock_time_matrix_client,
838        );
839
840        let bss_description = random_bss_description!(Wpa2);
841        let mut test_fut = pin!(logger.handle_connect_attempt(
842            fidl_ieee80211::StatusCode::Success,
843            &bss_description,
844            false
845        ));
846        assert_eq!(
847            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
848            Poll::Ready(())
849        );
850
851        let metrics =
852            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
853        assert_eq!(metrics.len(), 1);
854        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
855    }
856
857    #[test_case(1; "one_failure")]
858    #[test_case(2; "two_failures")]
859    #[fuchsia::test(add_test_attr = false)]
860    fn test_successive_connect_attempt_failures_cobalt_one_failure_then_success(n_failures: usize) {
861        let mut test_helper = setup_test();
862        let logger = ConnectDisconnectLogger::new(
863            test_helper.cobalt_proxy.clone(),
864            &test_helper.inspect_node,
865            &test_helper.inspect_metadata_node,
866            &test_helper.inspect_metadata_path,
867            &test_helper.mock_time_matrix_client,
868        );
869
870        let bss_description = random_bss_description!(Wpa2);
871        for _i in 0..n_failures {
872            let mut test_fut = pin!(logger.handle_connect_attempt(
873                fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
874                &bss_description,
875                false
876            ));
877            assert_eq!(
878                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
879                Poll::Ready(())
880            );
881        }
882
883        let metrics =
884            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
885        assert!(metrics.is_empty());
886
887        let mut test_fut = pin!(logger.handle_connect_attempt(
888            fidl_ieee80211::StatusCode::Success,
889            &bss_description,
890            false
891        ));
892        assert_eq!(
893            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
894            Poll::Ready(())
895        );
896
897        let metrics =
898            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
899        assert_eq!(metrics.len(), 1);
900        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
901
902        // Verify subsequent successes would report 0 failures
903        test_helper.clear_cobalt_events();
904        let mut test_fut = pin!(logger.handle_connect_attempt(
905            fidl_ieee80211::StatusCode::Success,
906            &bss_description,
907            false
908        ));
909        assert_eq!(
910            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
911            Poll::Ready(())
912        );
913        let metrics =
914            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
915        assert_eq!(metrics.len(), 1);
916        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
917    }
918
919    #[test_case(1; "one_failure")]
920    #[test_case(2; "two_failures")]
921    #[fuchsia::test(add_test_attr = false)]
922    fn test_successive_connect_attempt_failures_cobalt_one_failure_then_timeout(n_failures: usize) {
923        let mut test_helper = setup_test();
924        let logger = ConnectDisconnectLogger::new(
925            test_helper.cobalt_proxy.clone(),
926            &test_helper.inspect_node,
927            &test_helper.inspect_metadata_node,
928            &test_helper.inspect_metadata_path,
929            &test_helper.mock_time_matrix_client,
930        );
931
932        let bss_description = random_bss_description!(Wpa2);
933        for _i in 0..n_failures {
934            let mut test_fut = pin!(logger.handle_connect_attempt(
935                fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
936                &bss_description,
937                false
938            ));
939            assert_eq!(
940                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
941                Poll::Ready(())
942            );
943        }
944
945        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
946        let mut test_fut = pin!(logger.handle_periodic_telemetry());
947        assert_eq!(
948            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
949            Poll::Ready(())
950        );
951
952        // Not enough time has passed, so successive_connect_attempt_failures is not flushed yet
953        let metrics =
954            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
955        assert!(metrics.is_empty());
956
957        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(120_000_000_000));
958        let mut test_fut = pin!(logger.handle_periodic_telemetry());
959        assert_eq!(
960            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
961            Poll::Ready(())
962        );
963
964        let metrics =
965            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
966        assert_eq!(metrics.len(), 1);
967        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
968
969        // Verify timeout fires only once
970        test_helper.clear_cobalt_events();
971        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(240_000_000_000));
972        let mut test_fut = pin!(logger.handle_periodic_telemetry());
973        assert_eq!(
974            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
975            Poll::Ready(())
976        );
977        let metrics =
978            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
979        assert!(metrics.is_empty());
980    }
981
982    #[fuchsia::test]
983    fn test_zero_successive_connect_attempt_failures_on_suspend() {
984        let mut test_helper = setup_test();
985        let logger = ConnectDisconnectLogger::new(
986            test_helper.cobalt_proxy.clone(),
987            &test_helper.inspect_node,
988            &test_helper.inspect_metadata_node,
989            &test_helper.inspect_metadata_path,
990            &test_helper.mock_time_matrix_client,
991        );
992
993        let mut test_fut = pin!(logger.handle_suspend_imminent());
994        assert_eq!(
995            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
996            Poll::Ready(())
997        );
998
999        let metrics =
1000            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1001        assert!(metrics.is_empty());
1002    }
1003
1004    #[test_case(1; "one_failure")]
1005    #[test_case(2; "two_failures")]
1006    #[fuchsia::test(add_test_attr = false)]
1007    fn test_one_or_more_successive_connect_attempt_failures_on_suspend(n_failures: usize) {
1008        let mut test_helper = setup_test();
1009        let logger = ConnectDisconnectLogger::new(
1010            test_helper.cobalt_proxy.clone(),
1011            &test_helper.inspect_node,
1012            &test_helper.inspect_metadata_node,
1013            &test_helper.inspect_metadata_path,
1014            &test_helper.mock_time_matrix_client,
1015        );
1016
1017        let bss_description = random_bss_description!(Wpa2);
1018        for _i in 0..n_failures {
1019            let mut test_fut = pin!(logger.handle_connect_attempt(
1020                fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1021                &bss_description,
1022                false
1023            ));
1024            assert_eq!(
1025                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1026                Poll::Ready(())
1027            );
1028        }
1029
1030        let mut test_fut = pin!(logger.handle_suspend_imminent());
1031        assert_eq!(
1032            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1033            Poll::Ready(())
1034        );
1035
1036        let metrics =
1037            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1038        assert_eq!(metrics.len(), 1);
1039        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1040
1041        test_helper.clear_cobalt_events();
1042        let mut test_fut = pin!(logger.handle_suspend_imminent());
1043        assert_eq!(
1044            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1045            Poll::Ready(())
1046        );
1047
1048        // Count of successive failures shouldn't be logged again since it was already logged
1049        let metrics =
1050            test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1051        assert!(metrics.is_empty());
1052
1053        // Verify that the connection state has transitioned to ConnectFailed
1054        assert_matches!(*logger.connection_state.lock(), ConnectionState::ConnectFailed(_));
1055    }
1056
1057    #[fuchsia::test]
1058    fn test_log_disconnect_inspect() {
1059        let mut test_helper = setup_test();
1060        let logger = ConnectDisconnectLogger::new(
1061            test_helper.cobalt_proxy.clone(),
1062            &test_helper.inspect_node,
1063            &test_helper.inspect_metadata_node,
1064            &test_helper.inspect_metadata_path,
1065            &test_helper.mock_time_matrix_client,
1066        );
1067
1068        // Log the event
1069        let bss_description = fake_bss_description!(Open);
1070        let channel = bss_description.channel;
1071        let disconnect_info = DisconnectInfo {
1072            iface_id: 32,
1073            connected_duration: zx::BootDuration::from_seconds(30),
1074            is_sme_reconnecting: false,
1075            disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1076                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1077                reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1078            }),
1079            original_bss_desc: Box::new(bss_description),
1080            current_rssi_dbm: -30,
1081            current_snr_db: 25,
1082            current_channel: channel,
1083        };
1084        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1085        assert_eq!(
1086            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1087            Poll::Ready(())
1088        );
1089
1090        // Validate Inspect data
1091        let data = test_helper.get_inspect_data_tree();
1092        assert_data_tree!(@executor test_helper.exec, data, root: contains {
1093            test_stats: contains {
1094                metadata: contains {
1095                    connected_networks: {
1096                        "0": {
1097                            "@time": AnyNumericProperty,
1098                            "data": {
1099                                bssid: &*BSSID_REGEX,
1100                                ssid: &*SSID_REGEX,
1101                                ht_cap: AnyBytesProperty,
1102                                vht_cap: AnyBytesProperty,
1103                                protection: "Open",
1104                                is_wmm_assoc: AnyBoolProperty,
1105                                wmm_param: AnyBytesProperty,
1106                            }
1107                        }
1108                    },
1109                    disconnect_sources: {
1110                        "0": {
1111                            "@time": AnyNumericProperty,
1112                            "data": {
1113                                source: "ap",
1114                                reason: "UnspecifiedReason",
1115                                mlme_event_name: "DeauthenticateIndication",
1116                            }
1117                        }
1118                    },
1119                },
1120                disconnect_events: {
1121                    "0": {
1122                        "@time": AnyNumericProperty,
1123                        connected_duration: zx::BootDuration::from_seconds(30).into_nanos(),
1124                        disconnect_source_id: 0u64,
1125                        network_id: 0u64,
1126                        rssi_dbm: -30i64,
1127                        snr_db: 25i64,
1128                        channel: AnyStringProperty,
1129                    }
1130                }
1131            }
1132        });
1133
1134        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1135        assert_eq!(
1136            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1137            &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 1)),]
1138        );
1139        assert_eq!(
1140            &time_matrix_calls.drain::<u64>("disconnected_networks")[..],
1141            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1142        );
1143        assert_eq!(
1144            &time_matrix_calls.drain::<u64>("disconnect_sources")[..],
1145            &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1146        );
1147    }
1148
1149    #[fuchsia::test]
1150    fn test_log_disconnect_cobalt() {
1151        let mut test_helper = setup_test();
1152        let logger = ConnectDisconnectLogger::new(
1153            test_helper.cobalt_proxy.clone(),
1154            &test_helper.inspect_node,
1155            &test_helper.inspect_metadata_node,
1156            &test_helper.inspect_metadata_path,
1157            &test_helper.mock_time_matrix_client,
1158        );
1159
1160        // Log the event
1161        let disconnect_info = DisconnectInfo {
1162            connected_duration: zx::BootDuration::from_millis(300_000),
1163            disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1164                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1165                reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
1166            }),
1167            ..fake_disconnect_info()
1168        };
1169        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1170        assert_eq!(
1171            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1172            Poll::Ready(())
1173        );
1174
1175        let disconnect_count_metrics =
1176            test_helper.get_logged_metrics(metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID);
1177        assert_eq!(disconnect_count_metrics.len(), 1);
1178        assert_eq!(disconnect_count_metrics[0].payload, MetricEventPayload::Count(1));
1179
1180        let connected_duration_metrics =
1181            test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID);
1182        assert_eq!(connected_duration_metrics.len(), 1);
1183        assert_eq!(
1184            connected_duration_metrics[0].payload,
1185            MetricEventPayload::IntegerValue(300_000)
1186        );
1187
1188        let disconnect_by_reason_metrics =
1189            test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID);
1190        assert_eq!(disconnect_by_reason_metrics.len(), 1);
1191        assert_eq!(disconnect_by_reason_metrics[0].payload, MetricEventPayload::Count(1));
1192        assert_eq!(disconnect_by_reason_metrics[0].event_codes.len(), 2);
1193        assert_eq!(
1194            disconnect_by_reason_metrics[0].event_codes[0],
1195            fidl_ieee80211::ReasonCode::ApInitiated.into_primitive() as u32
1196        );
1197        assert_eq!(
1198            disconnect_by_reason_metrics[0].event_codes[1],
1199            metrics::ConnectivityWlanMetricDimensionDisconnectSource::Ap as u32
1200        );
1201    }
1202
1203    #[test_case(
1204        fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1205            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1206            reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1207        }),
1208        true;
1209        "ap_disconnect_source"
1210    )]
1211    #[test_case(
1212        fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1213            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1214            reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1215        }),
1216        true;
1217        "mlme_disconnect_source_not_link_failed"
1218    )]
1219    #[test_case(
1220        fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1221            mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1222            reason_code: fidl_ieee80211::ReasonCode::MlmeLinkFailed,
1223        }),
1224        false;
1225        "mlme_link_failed"
1226    )]
1227    #[test_case(
1228        fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::Unknown),
1229        false;
1230        "user_disconnect_source"
1231    )]
1232    #[fuchsia::test(add_test_attr = false)]
1233    fn test_log_disconnect_for_mobile_device_cobalt(
1234        disconnect_source: fidl_sme::DisconnectSource,
1235        should_log: bool,
1236    ) {
1237        let mut test_helper = setup_test();
1238        let logger = ConnectDisconnectLogger::new(
1239            test_helper.cobalt_proxy.clone(),
1240            &test_helper.inspect_node,
1241            &test_helper.inspect_metadata_node,
1242            &test_helper.inspect_metadata_path,
1243            &test_helper.mock_time_matrix_client,
1244        );
1245
1246        // Log the event
1247        let disconnect_info = DisconnectInfo { disconnect_source, ..fake_disconnect_info() };
1248        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1249        assert_eq!(
1250            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1251            Poll::Ready(())
1252        );
1253
1254        let metrics = test_helper
1255            .get_logged_metrics(metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID);
1256        if should_log {
1257            assert_eq!(metrics.len(), 1);
1258            assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1259            assert_matches!(*logger.connection_state.lock(), ConnectionState::Disconnected(_));
1260        } else {
1261            assert!(metrics.is_empty());
1262            assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
1263        }
1264    }
1265
1266    #[fuchsia::test]
1267    fn test_log_downtime_post_disconnect_on_reconnect() {
1268        let mut test_helper = setup_test();
1269        let logger = ConnectDisconnectLogger::new(
1270            test_helper.cobalt_proxy.clone(),
1271            &test_helper.inspect_node,
1272            &test_helper.inspect_metadata_node,
1273            &test_helper.inspect_metadata_path,
1274            &test_helper.mock_time_matrix_client,
1275        );
1276
1277        // Connect at 15th second
1278        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(15_000_000_000));
1279        let bss_description = random_bss_description!(Wpa2);
1280        let mut test_fut = pin!(logger.handle_connect_attempt(
1281            fidl_ieee80211::StatusCode::Success,
1282            &bss_description,
1283            false
1284        ));
1285        assert_eq!(
1286            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1287            Poll::Ready(())
1288        );
1289
1290        // Verify no downtime metric is logged on first successful connect
1291        let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
1292        assert!(metrics.is_empty());
1293
1294        // Verify that the connection state has transitioned to Connected
1295        assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1296
1297        // Disconnect at 25th second
1298        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(25_000_000_000));
1299        let disconnect_info = DisconnectInfo {
1300            connected_duration: zx::BootDuration::from_millis(300_000),
1301            disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1302                mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1303                reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
1304            }),
1305            ..fake_disconnect_info()
1306        };
1307        let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1308        assert_eq!(
1309            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1310            Poll::Ready(())
1311        );
1312
1313        // Verify that the connection state has transitioned to Disconnected
1314        assert_matches!(*logger.connection_state.lock(), ConnectionState::Disconnected(_));
1315
1316        // Reconnect at 60th second
1317        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
1318        let mut test_fut = pin!(logger.handle_connect_attempt(
1319            fidl_ieee80211::StatusCode::Success,
1320            &bss_description,
1321            false
1322        ));
1323        assert_eq!(
1324            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1325            Poll::Ready(())
1326        );
1327
1328        // Verify that downtime metric is logged
1329        let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
1330        assert_eq!(metrics.len(), 1);
1331        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(35_000));
1332
1333        // Verify that the connection state has transitioned to Connected
1334        assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1335    }
1336
1337    #[fuchsia::test]
1338    fn test_log_iface_destroyed() {
1339        let mut test_helper = setup_test();
1340        let logger = ConnectDisconnectLogger::new(
1341            test_helper.cobalt_proxy.clone(),
1342            &test_helper.inspect_node,
1343            &test_helper.inspect_metadata_node,
1344            &test_helper.inspect_metadata_path,
1345            &test_helper.mock_time_matrix_client,
1346        );
1347
1348        // Log connect event to move state to connected
1349        let bss_description = random_bss_description!();
1350        let mut test_fut = pin!(logger.handle_connect_attempt(
1351            fidl_ieee80211::StatusCode::Success,
1352            &bss_description,
1353            false
1354        ));
1355        assert_eq!(
1356            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1357            Poll::Ready(())
1358        );
1359
1360        // Verify that the connection state has transitioned to Connected
1361        assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1362
1363        // Log iface destroyed event to move state to idle
1364        let mut test_fut = pin!(logger.handle_iface_destroyed());
1365        assert_eq!(
1366            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1367            Poll::Ready(())
1368        );
1369
1370        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1371        assert_eq!(
1372            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1373            &[
1374                TimeMatrixCall::Fold(Timed::now(1 << 0)),
1375                TimeMatrixCall::Fold(Timed::now(1 << 3)),
1376                TimeMatrixCall::Fold(Timed::now(1 << 0))
1377            ]
1378        );
1379
1380        // Verify that the connection state has transitioned to Idle
1381        assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
1382    }
1383
1384    #[fuchsia::test]
1385    fn test_log_disable_client_connections() {
1386        let mut test_helper = setup_test();
1387        let logger = ConnectDisconnectLogger::new(
1388            test_helper.cobalt_proxy.clone(),
1389            &test_helper.inspect_node,
1390            &test_helper.inspect_metadata_node,
1391            &test_helper.inspect_metadata_path,
1392            &test_helper.mock_time_matrix_client,
1393        );
1394
1395        // Log connect event to move state to connected
1396        let bss_description = random_bss_description!();
1397        let mut test_fut = pin!(logger.handle_connect_attempt(
1398            fidl_ieee80211::StatusCode::Success,
1399            &bss_description,
1400            false
1401        ));
1402        assert_eq!(
1403            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1404            Poll::Ready(())
1405        );
1406
1407        // Verify that the connection state has transitioned to Connected
1408        assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1409
1410        // Disable client connections to move state to idle
1411        let mut test_fut =
1412            pin!(logger.handle_client_connections_toggle(&ClientConnectionsToggleEvent::Disabled));
1413        assert_eq!(
1414            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1415            Poll::Ready(())
1416        );
1417
1418        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1419        assert_eq!(
1420            &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1421            &[
1422                TimeMatrixCall::Fold(Timed::now(1 << 0)),
1423                TimeMatrixCall::Fold(Timed::now(1 << 3)),
1424                TimeMatrixCall::Fold(Timed::now(1 << 0))
1425            ]
1426        );
1427
1428        // Verify that the connection state has transitioned to Idle
1429        assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
1430    }
1431
1432    #[fuchsia::test]
1433    fn test_wlan_connectivity_states_credential_rejected() {
1434        let mut test_helper = setup_test();
1435        let logger = ConnectDisconnectLogger::new(
1436            test_helper.cobalt_proxy.clone(),
1437            &test_helper.inspect_node,
1438            &test_helper.inspect_metadata_node,
1439            &test_helper.inspect_metadata_path,
1440            &test_helper.mock_time_matrix_client,
1441        );
1442
1443        // Log connect failure with credential rejected to move state to idle
1444        let bss_description = random_bss_description!();
1445        let mut test_fut = pin!(logger.handle_connect_attempt(
1446            fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1447            &bss_description,
1448            true
1449        ));
1450        assert_eq!(
1451            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1452            Poll::Ready(())
1453        );
1454
1455        assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
1456    }
1457
1458    fn fake_disconnect_info() -> DisconnectInfo {
1459        let bss_description = random_bss_description!(Wpa2);
1460        let channel = bss_description.channel;
1461        DisconnectInfo {
1462            iface_id: 1,
1463            connected_duration: zx::BootDuration::from_hours(6),
1464            is_sme_reconnecting: false,
1465            disconnect_source: fidl_sme::DisconnectSource::User(
1466                fidl_sme::UserDisconnectReason::Unknown,
1467            ),
1468            original_bss_desc: bss_description.into(),
1469            current_rssi_dbm: -30,
1470            current_snr_db: 25,
1471            current_channel: channel,
1472        }
1473    }
1474}