1use crate::error::{ApiError, ApiResult};
2use crate::extractors::notification::Notification;
3use crate::headers::vapid::VapidHeaderWithKey;
4use crate::routers::RouterError;
5use actix_web::http::StatusCode;
6use autopush_common::db::client::DbClient;
7use autopush_common::metric_name::MetricName;
8use autopush_common::metrics::StatsdClientExt;
9use autopush_common::util::InsertOpt;
10use cadence::{Counted, StatsdClient, Timed};
11use std::collections::HashMap;
12use uuid::Uuid;
13
14use super::fcm::error::FcmError;
15
16pub fn build_message_data(notification: &Notification) -> ApiResult<HashMap<&'static str, String>> {
18 let mut message_data = HashMap::new();
19 message_data.insert("chid", notification.subscription.channel_id.to_string());
20
21 if let Some(data) = ¬ification.data {
23 message_data.insert("body", data.clone());
24 message_data.insert_opt("con", notification.headers.encoding.as_ref());
25 message_data.insert_opt("enc", notification.headers.encryption.as_ref());
26 message_data.insert_opt("cryptokey", notification.headers.crypto_key.as_ref());
27 message_data.insert_opt("enckey", notification.headers.encryption_key.as_ref());
28 trace!(
31 "🔍 Sending Reliability ID: {:?}",
32 notification.subscription.reliability_id
33 );
34 message_data.insert_opt("rid", notification.subscription.reliability_id.as_ref());
35 }
36
37 Ok(message_data)
38}
39
40pub fn message_size_check(data: &[u8], max_data: usize) -> Result<(), RouterError> {
43 if data.len() > max_data {
44 trace!("Data is too long by {} bytes", data.len() - max_data);
45 Err(RouterError::TooMuchData(data.len() - max_data))
46 } else {
47 Ok(())
48 }
49}
50
51pub async fn handle_error(
59 error: RouterError,
60 metrics: &StatsdClient,
61 db: &dyn DbClient,
62 platform: &str,
63 app_id: &str,
64 uaid: Uuid,
65 vapid: Option<VapidHeaderWithKey>,
66) -> ApiError {
67 match &error {
68 RouterError::Authentication => {
69 error!("Bridge authentication error");
70 incr_error_metric(
71 metrics,
72 platform,
73 app_id,
74 "authentication",
75 error.status(),
76 error.errno(),
77 );
78 }
79 RouterError::RequestTimeout => {
80 info!("Bridge timeout");
82 incr_error_metric(
83 metrics,
84 platform,
85 app_id,
86 "timeout",
87 error.status(),
88 error.errno(),
89 );
90 }
91 RouterError::Connect(e) => {
92 warn!("Bridge unavailable: {}", e);
93 incr_error_metric(
94 metrics,
95 platform,
96 app_id,
97 "connection_unavailable",
98 error.status(),
99 error.errno(),
100 );
101 }
102 RouterError::NotFound => {
103 debug!("Bridge recipient not found, removing user");
104 incr_error_metric(
105 metrics,
106 platform,
107 app_id,
108 "recipient_gone",
109 error.status(),
110 error.errno(),
111 );
112
113 if let Err(e) = db.remove_user(&uaid).await {
114 warn!("Error while removing user due to bridge not_found: {}", e);
115 }
116 }
117 RouterError::TooMuchData(_) => {
118 incr_error_metric(
120 metrics,
121 platform,
122 app_id,
123 "too_much_data",
124 error.status(),
125 error.errno(),
126 );
127 }
128 RouterError::Fcm(FcmError::Upstream {
129 error_code: status, ..
130 }) => incr_error_metric(
131 metrics,
132 platform,
133 app_id,
134 &format!("upstream_{status}"),
135 error.status(),
136 error.errno(),
137 ),
138
139 _ => {
140 warn!("Unknown error while sending bridge request: {error}");
141 incr_error_metric(
142 metrics,
143 platform,
144 app_id,
145 "unknown",
146 error.status(),
147 error.errno(),
148 );
149 }
150 }
151
152 let mut err = ApiError::from(error);
153
154 if let Some(Ok(claims)) = vapid.map(|v| v.vapid.claims()) {
155 let mut extras = err.extras.unwrap_or_default();
156 if let Some(sub) = claims.sub {
157 extras.extend([("sub".to_owned(), sub)]);
158 }
159 err.extras = Some(extras);
160 };
161 err
162}
163
164pub fn incr_error_metric(
166 metrics: &StatsdClient,
167 platform: &str,
168 app_id: &str,
169 reason: &str,
170 status: StatusCode,
171 errno: Option<usize>,
172) {
173 metrics
175 .incr_with_tags(MetricName::NotificationBridgeError)
176 .with_tag("platform", platform)
177 .with_tag("app_id", app_id)
178 .with_tag("reason", reason)
179 .with_tag("error", &status.to_string())
180 .with_tag("errno", &errno.unwrap_or(0).to_string())
181 .send();
182}
183
184pub fn incr_success_metrics(
186 metrics: &StatsdClient,
187 platform: &str,
188 app_id: &str,
189 notification: &Notification,
190) {
191 metrics
192 .incr_with_tags(MetricName::NotificationBridgeSent)
193 .with_tag("platform", platform)
194 .with_tag("app_id", app_id)
195 .send();
196 metrics
197 .count_with_tags(
198 MetricName::NotificationMessageData.as_ref(),
199 notification.data.as_ref().map(String::len).unwrap_or(0) as i64,
200 )
201 .with_tag("platform", platform)
202 .with_tag("app_id", app_id)
203 .with_tag("destination", "Direct")
204 .send();
205 metrics
206 .time_with_tags(
207 MetricName::NotificationTotalRequestTime.as_ref(),
208 (autopush_common::util::sec_since_epoch() - notification.timestamp) * 1000,
209 )
210 .with_tag("platform", platform)
211 .with_tag("app_id", app_id)
212 .send();
213}
214
215#[cfg(test)]
217pub mod tests {
218 use crate::extractors::notification::Notification;
219 use crate::extractors::notification_headers::NotificationHeaders;
220 use crate::extractors::routers::RouterType;
221 use crate::extractors::subscription::Subscription;
222 use autopush_common::db::User;
223 use std::collections::HashMap;
224 use uuid::Uuid;
225
226 pub const CHANNEL_ID: &str = "deadbeef-13f9-4639-87f9-2ff731824f34";
227
228 pub fn channel_id() -> Uuid {
230 Uuid::parse_str(CHANNEL_ID).unwrap()
231 }
232
233 pub fn make_notification(
235 router_data: HashMap<String, serde_json::Value>,
236 data: Option<String>,
237 router_type: RouterType,
238 ) -> Notification {
239 let user = User::builder()
240 .router_data(router_data)
241 .router_type(router_type.to_string())
242 .build()
243 .unwrap();
244 Notification {
245 message_id: "test-message-id".to_string(),
246 subscription: Subscription {
247 user,
248 channel_id: channel_id(),
249 vapid: None,
250 reliability_id: None,
251 },
252 headers: NotificationHeaders {
253 ttl: 0,
254 topic: Some("test-topic".to_string()),
255 encoding: Some("test-encoding".to_string()),
256 encryption: Some("test-encryption".to_string()),
257 encryption_key: Some("test-encryption-key".to_string()),
258 crypto_key: Some("test-crypto-key".to_string()),
259 },
260 timestamp: 0,
261 sort_key_timestamp: 0,
262 data,
263 #[cfg(feature = "reliable_report")]
264 reliable_state: None,
265 #[cfg(feature = "reliable_report")]
266 reliability_id: None,
267 }
268 }
269}