diff --git a/.circleci/config.yml b/.circleci/config.yml index 81febb751..70a24185a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -87,10 +87,10 @@ jobs: name: Check formatting # Rust 1.65+ is stricter about dead_code and flags auto-generated methods created by # state_machine_future. Since these are auto-generated, it's not possible to tag them as - # allowed. Adding `-A dead_code` until state_machine_future is removed. + # allowed. Adding `--allow=dead_code` until state_machine_future is removed. command: | cargo fmt -- --check - cargo clippy --all --all-targets --all-features -- -D warnings --deny=clippy::dbg_macro -A dead_code + cargo clippy --all --all-targets --all-features -- -D warnings --deny=clippy::dbg_macro --allow=dead_code - run: name: Integration tests command: py.test -v diff --git a/autoendpoint/src/db/client.rs b/autoendpoint/src/db/client.rs index df6f0bb0b..66065f54c 100644 --- a/autoendpoint/src/db/client.rs +++ b/autoendpoint/src/db/client.rs @@ -8,7 +8,7 @@ use autopush_common::db::{DynamoDbNotification, DynamoDbUser}; use autopush_common::notification::Notification; use autopush_common::util::sec_since_epoch; use autopush_common::{ddb_item, hashmap, val}; -use cadence::StatsdClient; +use cadence::{CountedExt, StatsdClient}; use rusoto_core::credential::StaticProvider; use rusoto_core::{HttpClient, Region, RusotoError}; use rusoto_dynamodb::{ @@ -270,6 +270,7 @@ impl DbClient for DbClientImpl { Ok(()) } + // Return the list of active channelIDs for a given user. async fn get_channels(&self, uaid: Uuid) -> DbResult> { // Channel IDs are stored in a special row in the message table, where // chidmessageid = " " @@ -366,6 +367,7 @@ impl DbClient for DbClientImpl { } async fn save_message(&self, uaid: Uuid, message: Notification) -> DbResult<()> { + let topic = message.topic.is_some().to_string(); let input = PutItemInput { item: serde_dynamodb::to_hashmap(&DynamoDbNotification::from_notif(&uaid, message))?, table_name: self.message_table.clone(), @@ -378,7 +380,13 @@ impl DbClient for DbClientImpl { retryable_putitem_error(self.metrics.clone()), ) .await?; - + { + // Build the metric report + let mut metric = self.metrics.incr_with_tags("notification.message.stored"); + metric = metric.with_tag("topic", &topic); + // TODO: include `internal` if meta is set. + metric.send(); + } Ok(()) } diff --git a/autoendpoint/src/extractors/notification.rs b/autoendpoint/src/extractors/notification.rs index 1f8731d2b..1d2444ad9 100644 --- a/autoendpoint/src/extractors/notification.rs +++ b/autoendpoint/src/extractors/notification.rs @@ -17,13 +17,17 @@ use uuid::Uuid; /// Extracts notification data from `Subscription` and request data #[derive(Clone, Debug)] pub struct Notification { + /// Unique message_id for this notification pub message_id: String, + /// The subscription information block pub subscription: Subscription, + /// Set of associated crypto headers pub headers: NotificationHeaders, /// UNIX timestamp in seconds pub timestamp: u64, /// UNIX timestamp in milliseconds pub sort_key_timestamp: u64, + /// The encrypted notification body pub data: Option, } diff --git a/autoendpoint/src/routers/common.rs b/autoendpoint/src/routers/common.rs index 477a7c1c3..ee3683bfe 100644 --- a/autoendpoint/src/routers/common.rs +++ b/autoendpoint/src/routers/common.rs @@ -4,7 +4,7 @@ use crate::extractors::notification::Notification; use crate::routers::RouterError; use actix_web::http::StatusCode; use autopush_common::util::InsertOpt; -use cadence::{Counted, CountedExt, StatsdClient}; +use cadence::{Counted, CountedExt, StatsdClient, Timed}; use std::collections::HashMap; use uuid::Uuid; @@ -173,6 +173,14 @@ pub fn incr_success_metrics( .with_tag("app_id", app_id) .with_tag("destination", "Direct") .send(); + metrics + .time_with_tags( + "notification.total_request_time", + (autopush_common::util::sec_since_epoch() - notification.timestamp) * 1000, + ) + .with_tag("platform", platform) + .with_tag("app_id", app_id) + .send(); } /// Common router test code diff --git a/autoendpoint/src/routers/webpush.rs b/autoendpoint/src/routers/webpush.rs index 4c33a9ece..9b54e5589 100644 --- a/autoendpoint/src/routers/webpush.rs +++ b/autoendpoint/src/routers/webpush.rs @@ -5,7 +5,7 @@ use crate::extractors::router_data_input::RouterDataInput; use crate::routers::{Router, RouterError, RouterResponse}; use async_trait::async_trait; use autopush_common::db::DynamoDbUser; -use cadence::{Counted, CountedExt, StatsdClient}; +use cadence::{Counted, CountedExt, StatsdClient, Timed}; use reqwest::{Response, StatusCode}; use serde_json::Value; use std::collections::hash_map::RandomState; @@ -73,10 +73,16 @@ impl Router for WebPushRouter { } if notification.headers.ttl == 0 { + let topic = notification.headers.topic.is_some().to_string(); trace!( "Notification has a TTL of zero and was not successfully \ delivered, dropping it" ); + self.metrics + .incr_with_tags("notification.message.expired") + // TODO: include `internal` if meta is set. + .with_tag("topic", &topic) + .send(); return Ok(self.make_delivered_response(notification)); } @@ -117,6 +123,16 @@ impl Router for WebPushRouter { trace!("Response = {:?}", response); if response.status() == 200 { trace!("Node has delivered the message"); + self.metrics + .time_with_tags( + "notification.total_request_time", + (notification.timestamp - autopush_common::util::sec_since_epoch()) + * 1000, + ) + .with_tag("platform", "websocket") + .with_tag("app_id", "direct") + .send(); + Ok(self.make_delivered_response(notification)) } else { trace!("Node has not delivered the message, returning stored response"); diff --git a/autopush-common/src/db/commands.rs b/autopush-common/src/db/commands.rs index 132b516a8..73d7b9469 100644 --- a/autopush-common/src/db/commands.rs +++ b/autopush-common/src/db/commands.rs @@ -75,6 +75,7 @@ pub fn list_tables_sync( .chain_err(|| "Unable to list tables") } +/// Pull all pending messages for the user from storage pub fn fetch_messages( ddb: DynamoDbClient, metrics: Arc, @@ -136,6 +137,8 @@ pub fn fetch_messages( }) } +/// Pull messages older than a given timestamp for a given user. +/// This also returns the latest message timestamp. pub fn fetch_timestamp_messages( ddb: DynamoDbClient, metrics: Arc, @@ -191,6 +194,7 @@ pub fn fetch_timestamp_messages( }) } +/// Drop all user information from the Router table. pub fn drop_user( ddb: DynamoDbClient, uaid: &Uuid, @@ -208,6 +212,7 @@ pub fn drop_user( .chain_err(|| "Error dropping user") } +/// Get the user information from the Router table. pub fn get_uaid( ddb: DynamoDbClient, uaid: &Uuid, @@ -223,6 +228,7 @@ pub fn get_uaid( .chain_err(|| "Error fetching user") } +/// Register a user into the Router table. pub fn register_user( ddb: DynamoDbClient, user: &DynamoDbUser, @@ -264,6 +270,8 @@ pub fn register_user( .chain_err(|| "Error storing user record") } +/// Update the user's message month (Note: This is legacy for DynamoDB, but may still +/// be used by Stand Alone systems.) pub fn update_user_message_month( ddb: DynamoDbClient, uaid: &Uuid, @@ -294,6 +302,7 @@ pub fn update_user_message_month( .chain_err(|| "Error updating user message month") } +/// Return all known Channels for a given User. pub fn all_channels( ddb: DynamoDbClient, uaid: &Uuid, @@ -324,6 +333,7 @@ pub fn all_channels( .or_else(|_err| future::ok(HashSet::new())) } +/// Save the current list of Channels for a given user. pub fn save_channels( ddb: DynamoDbClient, uaid: &Uuid, @@ -357,6 +367,7 @@ pub fn save_channels( .chain_err(|| "Error saving channels") } +/// Remove a specific channel from the list of known channels for a given User pub fn unregister_channel_id( ddb: DynamoDbClient, uaid: &Uuid, @@ -385,6 +396,7 @@ pub fn unregister_channel_id( .chain_err(|| "Error unregistering channel") } +/// Respond with user information for a given user. #[allow(clippy::too_many_arguments)] pub fn lookup_user( ddb: DynamoDbClient, diff --git a/autopush-common/src/db/mod.rs b/autopush-common/src/db/mod.rs index 6b7b591bf..a91da26da 100644 --- a/autopush-common/src/db/mod.rs +++ b/autopush-common/src/db/mod.rs @@ -3,7 +3,7 @@ use std::env; use std::sync::Arc; use uuid::Uuid; -use cadence::StatsdClient; +use cadence::{Counted, CountedExt, StatsdClient}; use futures::{future, Future}; use futures_backoff::retry_if; use rusoto_core::{HttpClient, Region}; @@ -322,6 +322,7 @@ impl DynamoStorage { message_month: String, message: Notification, ) -> impl Future { + let topic = message.topic.is_some().to_string(); let ddb = self.ddb.clone(); let put_item = PutItemInput { item: serde_dynamodb::to_hashmap(&DynamoDbNotification::from_notif(uaid, message)) @@ -329,12 +330,18 @@ impl DynamoStorage { table_name: message_month, ..Default::default() }; - + let metrics = self.metrics.clone(); retry_if( move || ddb.put_item(put_item.clone()), retryable_putitem_error, ) - .and_then(|_| future::ok(())) + .and_then(move |_| { + let mut metric = metrics.incr_with_tags("notification.message.stored"); + // TODO: include `internal` if meta is set. + metric = metric.with_tag("topic", &topic); + metric.send(); + future::ok(()) + }) .chain_err(|| "Error saving notification") } @@ -346,9 +353,15 @@ impl DynamoStorage { messages: Vec, ) -> impl Future { let ddb = self.ddb.clone(); + let metrics = self.metrics.clone(); let put_items: Vec = messages .into_iter() .filter_map(|n| { + // eventually include `internal` if `meta` defined. + metrics + .incr_with_tags("notification.message.stored") + .with_tag("topic", &n.topic.is_some().to_string()) + .send(); serde_dynamodb::to_hashmap(&DynamoDbNotification::from_notif(uaid, n)) .ok() .map(|hm| WriteRequest { @@ -386,7 +399,9 @@ impl DynamoStorage { uaid: &Uuid, notif: &Notification, ) -> impl Future { + let topic = notif.topic.is_some().to_string(); let ddb = self.ddb.clone(); + let metrics = self.metrics.clone(); let delete_input = DeleteItemInput { table_name: table_name.to_string(), key: ddb_item! { @@ -400,10 +415,17 @@ impl DynamoStorage { move || ddb.delete_item(delete_input.clone()), retryable_delete_error, ) - .and_then(|_| future::ok(())) + .and_then(move |_| { + let mut metric = metrics.incr_with_tags("notification.message.deleted"); + // TODO: include `internal` if meta is set. + metric = metric.with_tag("topic", &topic); + metric.send(); + future::ok(()) + }) .chain_err(|| "Error deleting notification") } + /// Check to see if we have pending messages and return them if we do. pub fn check_storage( &self, table_name: &str, @@ -426,11 +448,16 @@ impl DynamoStorage { let table_name = table_name.to_string(); let ddb = self.ddb.clone(); let metrics = self.metrics.clone(); + let rmetrics = metrics.clone(); response.and_then(move |resp| -> MyFuture<_> { // Return now from this future if we have messages if !resp.messages.is_empty() { debug!("Topic message returns: {:?}", resp.messages); + rmetrics + .count_with_tags("notification.message.retrieved", resp.messages.len() as i64) + .with_tag("topic", "true") + .send(); return Box::new(future::ok(CheckStorageResponse { include_topic: true, messages: resp.messages, @@ -461,6 +488,10 @@ impl DynamoStorage { // If we didn't get a timestamp off the last query, use the original // value if passed one let timestamp = resp.timestamp.or(timestamp); + rmetrics + .count_with_tags("notification.message.retrieved", resp.messages.len() as i64) + .with_tag("topic", "false") + .send(); Ok(CheckStorageResponse { include_topic: false, messages: resp.messages, @@ -532,6 +563,8 @@ impl DynamoStorage { } } +/// Get the list of current, valid message tables (Note: This is legacy for DynamoDB, but may still +/// be used for Stand Alone systems ) pub fn list_message_tables(ddb: &DynamoDbClient, prefix: &str) -> Result> { let mut names: Vec = Vec::new(); let mut start_key = None; diff --git a/autopush-common/src/db/models.rs b/autopush-common/src/db/models.rs index b55ca812c..a650e33e4 100644 --- a/autopush-common/src/db/models.rs +++ b/autopush-common/src/db/models.rs @@ -105,7 +105,7 @@ impl Default for DynamoDbUser { } } -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct DynamoDbNotification { // DynamoDB #[serde(serialize_with = "uuid_serializer")] @@ -145,6 +145,24 @@ pub struct DynamoDbNotification { updateid: Option, } +/// Ensure that the default for 'stored' is true. +impl Default for DynamoDbNotification { + fn default() -> Self { + Self { + uaid: Uuid::default(), + chidmessageid: String::default(), + current_timestamp: None, + chids: None, + timestamp: None, + expiry: 0, + ttl: None, + data: None, + headers: None, + updateid: None, + } + } +} + impl DynamoDbNotification { fn parse_sort_key(key: &str) -> Result { lazy_static! { diff --git a/autopush/src/client.rs b/autopush/src/client.rs index a562b650a..00d0dd0c1 100644 --- a/autopush/src/client.rs +++ b/autopush/src/client.rs @@ -27,7 +27,7 @@ use autopush_common::util::{ms_since_epoch, sec_since_epoch}; use crate::megaphone::{Broadcast, BroadcastSubs}; use crate::server::protocol::{ClientMessage, ServerMessage, ServerNotification}; use crate::server::Server; -use crate::user_agent::parse_user_agent; +use crate::user_agent::UserAgentInfo; // Created and handed to the AutopushServer pub struct RegisteredClient { @@ -163,6 +163,7 @@ pub struct WebPushClient { last_ping: u64, stats: SessionStatistics, deferred_user_registration: Option, + ua_info: UserAgentInfo, } impl Default for WebPushClient { @@ -182,6 +183,7 @@ impl Default for WebPushClient { last_ping: Default::default(), stats: Default::default(), deferred_user_registration: Default::default(), + ua_info: Default::default(), } } } @@ -310,7 +312,6 @@ where AwaitSessionComplete { auth_state_machine: AuthClientStateFuture, srv: Rc, - user_agent: String, webpush: Rc>, }, @@ -318,7 +319,6 @@ where AwaitRegistryDisconnect { response: MyFuture<()>, srv: Rc, - user_agent: String, webpush: Rc>, error: Option, }, @@ -473,6 +473,7 @@ where ..Default::default() }, deferred_user_registration, + ua_info: UserAgentInfo::from(user_agent.as_ref()), ..Default::default() })); @@ -509,7 +510,6 @@ where let AwaitRegistryConnect { srv, ws, - user_agent, webpush, broadcast_subs, .. @@ -528,7 +528,6 @@ where transition!(AwaitSessionComplete { auth_state_machine, srv, - user_agent, webpush, }) } @@ -553,12 +552,7 @@ where } }; - let AwaitSessionComplete { - srv, - user_agent, - webpush, - .. - } = session_complete.take(); + let AwaitSessionComplete { srv, webpush, .. } = session_complete.take(); let response = srv .clients @@ -567,7 +561,6 @@ where transition!(AwaitRegistryDisconnect { response, srv, - user_agent, webpush, error, }) @@ -581,7 +574,6 @@ where let AwaitRegistryDisconnect { srv, - user_agent, webpush, error, .. @@ -600,16 +592,17 @@ where } let now = ms_since_epoch(); let elapsed = (now - webpush.connected_at) / 1_000; - let (ua_result, metrics_os, metrics_browser) = parse_user_agent(&user_agent); + let ua_info = webpush.ua_info.clone(); // dogstatsd doesn't support timers: use histogram instead srv.metrics .time_with_tags("ua.connection.lifespan", elapsed) - .with_tag("ua_os_family", metrics_os) - .with_tag("ua_browser_family", metrics_browser) + .with_tag("ua_os_family", &ua_info.metrics_os) + .with_tag("ua_browser_family", &ua_info.metrics_browser) .send(); // Log out the sentry message if applicable and convert to error msg let error = if let Some(ref err) = error { + let ua_info = ua_info.clone(); let mut event = event_from_error_chain(err); event.user = Some(sentry::User { id: Some(webpush.uaid.as_simple().to_string()), @@ -617,19 +610,19 @@ where }); event .tags - .insert("ua_name".to_string(), ua_result.name.to_string()); + .insert("ua_name".to_string(), ua_info.browser_name); event .tags - .insert("ua_os_family".to_string(), metrics_os.to_string()); + .insert("ua_os_family".to_string(), ua_info.metrics_os); event .tags - .insert("ua_os_ver".to_string(), ua_result.os_version.to_string()); + .insert("ua_os_ver".to_string(), ua_info.os_version); event .tags - .insert("ua_browser_family".to_string(), metrics_browser.to_string()); + .insert("ua_browser_family".to_string(), ua_info.metrics_browser); event .tags - .insert("ua_browser_ver".to_string(), ua_result.version.to_string()); + .insert("ua_browser_ver".to_string(), ua_info.browser_version); sentry::capture_event(event); err.display_chain().to_string() } else { @@ -658,12 +651,12 @@ where "uaid_reset" => stats.uaid_reset, "existing_uaid" => stats.existing_uaid, "connection_type" => &stats.connection_type, - "ua_name" => ua_result.name, - "ua_os_family" => metrics_os, - "ua_os_ver" => ua_result.os_version.into_owned(), - "ua_browser_family" => metrics_browser, - "ua_browser_ver" => ua_result.version, - "ua_category" => ua_result.category, + "ua_name" => ua_info.browser_name, + "ua_os_family" => ua_info.metrics_os, + "ua_os_ver" => ua_info.os_version, + "ua_browser_family" => ua_info.metrics_browser, + "ua_browser_ver" => ua_info.browser_version, + "ua_category" => ua_info.category, "connection_time" => elapsed, "direct_acked" => stats.direct_acked, "direct_storage" => stats.direct_storage, @@ -1103,7 +1096,7 @@ where webpush.unacked_direct_notifs.push(notif.clone()); } debug!("Got a notification to send, sending!"); - emit_metrics_for_send(&data.srv.metrics, ¬if, "Direct"); + emit_metrics_for_send(&data.srv.metrics, ¬if, "Direct", &webpush.ua_info); transition!(Send { smessages: vec![ServerMessage::Notification(notif)], data, @@ -1220,7 +1213,9 @@ where .extend(messages.iter().cloned()); let smessages: Vec<_> = messages .into_iter() - .inspect(|msg| emit_metrics_for_send(&data.srv.metrics, msg, "Stored")) + .inspect(|msg| { + emit_metrics_for_send(&data.srv.metrics, msg, "Stored", &webpush.ua_info) + }) .map(ServerMessage::Notification) .collect(); webpush.sent_from_storage += smessages.len() as u32; @@ -1340,15 +1335,25 @@ where } } -fn emit_metrics_for_send(metrics: &StatsdClient, notif: &Notification, source: &'static str) { - if notif.topic.is_some() { - metrics.incr("ua.notification.topic").ok(); - } +fn emit_metrics_for_send( + metrics: &StatsdClient, + notif: &Notification, + source: &'static str, + user_agent_info: &UserAgentInfo, +) { + metrics + .incr_with_tags("ua.notification.sent") + .with_tag("source", source) + .with_tag("topic", ¬if.topic.is_some().to_string()) + .with_tag("os", &user_agent_info.metrics_os) + // TODO: include `internal` if meta is set + .send(); metrics .count_with_tags( "ua.message_data", notif.data.as_ref().map_or(0, |data| data.len() as i64), ) .with_tag("source", source) + .with_tag("os", &user_agent_info.metrics_os) .send(); } diff --git a/autopush/src/user_agent.rs b/autopush/src/user_agent.rs index 17e448430..b69c825f2 100644 --- a/autopush/src/user_agent.rs +++ b/autopush/src/user_agent.rs @@ -1,4 +1,4 @@ -use woothee::parser::{Parser, WootheeResult}; +use woothee::parser::Parser; // List of valid user-agent attributes to keep, anything not in this // list is considered 'Other'. We log the user-agent on connect always @@ -11,74 +11,90 @@ const VALID_UA_BROWSER: &[&str] = &["Chrome", "Firefox", "Safari", "Opera"]; // field). Windows has many values and we only care that its Windows const VALID_UA_OS: &[&str] = &["Firefox OS", "Linux", "Mac OSX"]; -pub fn parse_user_agent(agent: &str) -> (WootheeResult, &str, &str) { - let parser = Parser::new(); - let wresult = parser.parse(agent).unwrap_or_else(|| WootheeResult { - name: "", - category: "", - os: "", - os_version: "".into(), - browser_type: "", - version: "", - vendor: "", - }); +#[derive(Clone, Debug, Default)] +pub struct UserAgentInfo { + _user_agent_string: String, + pub category: String, + pub browser_name: String, + pub browser_version: String, + pub metrics_browser: String, + pub metrics_os: String, + pub os_version: String, + pub os: String, +} + +impl From<&str> for UserAgentInfo { + fn from(user_agent_string: &str) -> Self { + let parser = Parser::new(); + let wresult = parser.parse(user_agent_string).unwrap_or_default(); - // Determine a base os/browser for metrics' tags - let metrics_os = if wresult.os.starts_with("Windows") { - "Windows" - } else if VALID_UA_OS.contains(&wresult.os) { - wresult.os - } else { - "Other" - }; - let metrics_browser = if VALID_UA_BROWSER.contains(&wresult.name) { - wresult.name - } else { - "Other" - }; - (wresult, metrics_os, metrics_browser) + // Determine a base os/browser for metrics' tags + let metrics_os = if wresult.os.starts_with("Windows") { + "Windows" + } else if VALID_UA_OS.contains(&wresult.os) { + wresult.os + } else { + "Other" + }; + let metrics_browser = if VALID_UA_BROWSER.contains(&wresult.name) { + wresult.name + } else { + "Other" + }; + + Self { + category: wresult.category.to_owned(), + browser_name: wresult.name.to_owned(), + browser_version: wresult.version.to_owned(), + metrics_browser: metrics_browser.to_owned(), + metrics_os: metrics_os.to_owned(), + os_version: wresult.os_version.to_string(), + os: wresult.os.to_owned(), + _user_agent_string: user_agent_string.to_owned(), + } + } } #[cfg(test)] mod tests { - use super::parse_user_agent; + use super::UserAgentInfo; #[test] fn test_linux() { let agent = r#"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.2) Gecko/20090807 Mandriva Linux/1.9.1.2-1.1mud2009.1 (2009.1) Firefox/3.5.2 FirePHP/0.3,gzip(gfe),gzip(gfe)"#; - let (ua_result, metrics_os, metrics_browser) = parse_user_agent(agent); - assert_eq!(metrics_os, "Linux"); + let ua_result = UserAgentInfo::from(agent); + assert_eq!(ua_result.metrics_os, "Linux"); assert_eq!(ua_result.os, "Linux"); - assert_eq!(metrics_browser, "Firefox"); + assert_eq!(ua_result.metrics_browser, "Firefox"); } #[test] fn test_windows() { let agent = r#"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 (.NET CLR 3.5.30729)"#; - let (ua_result, metrics_os, metrics_browser) = parse_user_agent(agent); - assert_eq!(metrics_os, "Windows"); + let ua_result = UserAgentInfo::from(agent); + assert_eq!(ua_result.metrics_os, "Windows"); assert_eq!(ua_result.os, "Windows 7"); - assert_eq!(metrics_browser, "Firefox"); + assert_eq!(ua_result.metrics_browser, "Firefox"); } #[test] fn test_osx() { let agent = r#"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.5; rv:2.1.1) Gecko/ Firefox/5.0.1"#; - let (ua_result, metrics_os, metrics_browser) = parse_user_agent(agent); - assert_eq!(metrics_os, "Mac OSX"); + let ua_result = UserAgentInfo::from(agent); + assert_eq!(ua_result.metrics_os, "Mac OSX"); assert_eq!(ua_result.os, "Mac OSX"); - assert_eq!(metrics_browser, "Firefox"); + assert_eq!(ua_result.metrics_browser, "Firefox"); } #[test] fn test_other() { let agent = r#"BlackBerry9000/4.6.0.167 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/102"#; - let (ua_result, metrics_os, metrics_browser) = parse_user_agent(agent); - assert_eq!(metrics_os, "Other"); + let ua_result = UserAgentInfo::from(agent); + assert_eq!(ua_result.metrics_os, "Other"); assert_eq!(ua_result.os, "BlackBerry"); - assert_eq!(metrics_browser, "Other"); - assert_eq!(ua_result.name, "UNKNOWN"); + assert_eq!(ua_result.metrics_browser, "Other"); + assert_eq!(ua_result.browser_name, "UNKNOWN"); } }