webserver_base/
axum_plausible_analytics.rs1use axum::http::{HeaderMap, StatusCode};
2use plausible_rs::{EventHeaders, EventPayload, PAGEVIEW_EVENT, Plausible};
3use reqwest::Client;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::net::SocketAddr;
7use std::sync::Arc;
8use tracing::{error, info, instrument, warn};
9
10use crate::base_settings::{BaseSettings, Environment};
11
12#[derive(Debug, Serialize, Deserialize)]
13pub struct RequestPayload {
14 pub user_agent: String,
15 pub url: String,
16 pub referrer: String,
17 pub screen_width: usize,
18}
19
20pub struct AxumPlausibleAnalyticsHandler {
21 plausible_client: Plausible,
22}
23
24impl AxumPlausibleAnalyticsHandler {
25 #[must_use]
26 #[instrument(skip_all)]
27 pub fn new_with_client(http_client: Client) -> Self {
28 Self {
29 plausible_client: Plausible::new_with_client(http_client),
30 }
31 }
32
33 #[instrument(skip_all)]
34 pub async fn handle(
35 self: Arc<Self>,
36 headers: HeaderMap,
37 settings: BaseSettings,
38 addr: SocketAddr,
39 incoming_payload: RequestPayload,
40 ) -> StatusCode {
41 let domain: String = if settings.environment == Environment::Production {
43 settings.analytics_domain.clone()
44 } else {
45 String::from("test.toddgriffin.me")
46 };
47 let outgoing_payload: EventPayload = EventPayload::builder(
48 domain,
49 PAGEVIEW_EVENT.to_string(),
50 incoming_payload.url.clone(),
51 )
52 .referrer(incoming_payload.referrer.clone())
53 .screen_width(incoming_payload.screen_width)
54 .build();
55
56 let real_client_ip: String = Self::resolve_true_client_ip_address(addr, headers);
58 let headers: EventHeaders =
59 EventHeaders::new(incoming_payload.user_agent.clone(), real_client_ip);
60
61 info!(
62 "Making Plausible Analytics calls with headers={:?} and body={:?}",
63 headers.clone(),
64 outgoing_payload.clone()
65 );
66 match self.plausible_client.event(headers, outgoing_payload).await {
68 Ok(bytes) => {
69 info!(
70 "Plausible Analytics call was a success: {}",
71 String::from_utf8_lossy(&bytes)
72 );
73 StatusCode::OK
74 }
75 Err(e) => {
76 error!("Failed Plausible Analytics call: {}", e);
77 StatusCode::INTERNAL_SERVER_ERROR
78 }
79 }
80 }
81
82 #[instrument(skip_all)]
84 fn resolve_true_client_ip_address(socket_addr: SocketAddr, header_map: HeaderMap) -> String {
85 let prioritized_headers: Vec<&str> = vec![
88 "True-Client-IP",
90 "CF-Connecting-IP",
91 "X-Forwarded-For",
93 "X-Real-IP",
94 "Forwarded",
95 "socket_addr",
97 ];
98
99 let prioritized_headers_values: HashMap<&str, Option<String>> = prioritized_headers.iter().map(|prioritized_header| {
101 if *prioritized_header == "socket_addr" {
102 return (*prioritized_header, Some(socket_addr.ip().to_string()));
105 }
106
107 let header_value: Option<String> = match header_map.get(*prioritized_header) {
108 Some(header_value) => {
110 match header_value.to_str() {
111 Ok(header_value) => {
113 if *prioritized_header == "X-Forwarded-For" {
114 info!("full HTTP 'X-Forwarded-For' header IP list: {header_value}");
115
116 let parts: Vec<&str> = header_value.split(',').collect();
118
119 let x_forwarded_for_client_ip: Option<&&str> = parts.first();
122 if let Some(x_forwarded_for_client_ip) = x_forwarded_for_client_ip {
123 let x_forwarded_for: String = (*x_forwarded_for_client_ip).to_string();
124 Some(x_forwarded_for)
125 } else {
126 None
128 }
129 } else {
130 Some(header_value.to_string())
132 }
133 }
134 Err(to_str_error) => {
136 error!("failed to parse HTTP '{prioritized_header}' header value: {to_str_error}");
137 None
138 }
139 }
140 }
141 None => {
143 None
144 },
145 };
146 (*prioritized_header, header_value)
147 }).collect();
148 info!(
149 "Client IP headers: {:?}",
150 prioritized_headers_values.clone()
151 );
152
153 for prioritized_header in prioritized_headers {
155 if let Some(Some(header_value)) = prioritized_headers_values.get(prioritized_header) {
156 info!(
157 "chose HTTP '{prioritized_header}' header for true client IP: '{header_value}'"
158 );
159 return header_value.to_string();
160 }
161 }
162
163 error!("failed to find any prioritized HTTP headers for true client IP address");
165 prioritized_headers_values
166 .get("socket_addr")
167 .unwrap()
168 .as_ref()
169 .unwrap()
170 .to_string()
171 }
172}