relay_filter/
interface.rs

1//! This module contains the trait for items that can be filtered by Inbound Filters, plus
2//! the implementation for [`Event`].
3use relay_conventions::{BROWSER_NAME, BROWSER_VERSION, RELEASE, SEGMENT_NAME};
4use url::Url;
5
6use relay_event_schema::protocol::{
7    Attributes, Csp, Event, EventType, Exception, LogEntry, OurLog, Replay, SessionAggregates,
8    SessionUpdate, Span, SpanV2, TraceMetric, Values,
9};
10
11/// A user agent returned from [`Filterable::user_agent`].
12#[derive(Clone, Debug, Default)]
13pub struct UserAgent<'a> {
14    /// The raw, unparsed user agent string, if available.
15    pub raw: Option<&'a str>,
16    /// A already fully parsed user agent, if available.
17    pub parsed: Option<relay_ua::UserAgent<'a>>,
18}
19
20impl<'a> UserAgent<'a> {
21    /// Parses the raw user agent, if it is not already parsed and returns it.
22    pub fn parsed(&mut self) -> Option<&'_ relay_ua::UserAgent<'a>> {
23        match &mut self.parsed {
24            Some(parsed) => Some(parsed),
25            parsed @ None => {
26                *parsed = Some(relay_ua::parse_user_agent(self.raw?));
27                parsed.as_ref()
28            }
29        }
30    }
31}
32
33/// A data item to which filters can be applied.
34pub trait Filterable {
35    /// The CSP report contained in the item. Only for CSP reports.
36    fn csp(&self) -> Option<&Csp> {
37        None
38    }
39
40    /// The exception values of the item. Only for error events.
41    fn exceptions(&self) -> Option<&Values<Exception>> {
42        None
43    }
44
45    /// The IP address of the client that sent the data.
46    fn ip_addr(&self) -> Option<&str> {
47        None
48    }
49
50    /// The logentry message. Only for error events.
51    fn logentry(&self) -> Option<&LogEntry> {
52        None
53    }
54
55    /// The release string of the data item.
56    fn release(&self) -> Option<&str> {
57        None
58    }
59
60    /// The transaction name. Only for transaction events.
61    fn transaction(&self) -> Option<&str> {
62        None
63    }
64
65    /// The URL from which the request originates. Used for localhost filtering.
66    fn url(&self) -> Option<Url> {
67        None
68    }
69
70    /// The user agent of the client that sent the data.
71    fn user_agent(&self) -> UserAgent<'_> {
72        Default::default()
73    }
74
75    /// Retrieves a request headers from the item.
76    ///
77    /// For example this is used for localhost filtering by inspecting relevant headers that may
78    /// be included in the `request.headers` of an error.
79    ///
80    /// This **does not** return header information from the request that reached relay.
81    fn header(&self, _header_name: &str) -> Option<&str> {
82        None
83    }
84}
85
86impl Filterable for Event {
87    fn csp(&self) -> Option<&Csp> {
88        if self.ty.value() != Some(&EventType::Csp) {
89            return None;
90        }
91        self.csp.value()
92    }
93
94    fn exceptions(&self) -> Option<&Values<Exception>> {
95        self.exceptions.value()
96    }
97
98    fn ip_addr(&self) -> Option<&str> {
99        let user = self.user.value()?;
100        Some(user.ip_address.value()?.as_ref())
101    }
102
103    fn logentry(&self) -> Option<&LogEntry> {
104        self.logentry.value()
105    }
106
107    fn release(&self) -> Option<&str> {
108        self.release.as_str()
109    }
110
111    fn transaction(&self) -> Option<&str> {
112        if self.ty.value() != Some(&EventType::Transaction) {
113            return None;
114        }
115        self.transaction.as_str()
116    }
117
118    fn url(&self) -> Option<Url> {
119        let url_str = self.request.value()?.url.value()?;
120        Url::parse(url_str).ok()
121    }
122
123    fn user_agent(&self) -> UserAgent<'_> {
124        UserAgent {
125            raw: self.user_agent(),
126            parsed: None,
127        }
128    }
129
130    fn header(&self, header_name: &str) -> Option<&str> {
131        self.request
132            .value()?
133            .headers
134            .value()?
135            .get_header(header_name)
136    }
137}
138
139impl Filterable for Replay {
140    fn ip_addr(&self) -> Option<&str> {
141        let user = self.user.value()?;
142        Some(user.ip_address.value()?.as_ref())
143    }
144
145    fn release(&self) -> Option<&str> {
146        self.release.as_str()
147    }
148
149    fn url(&self) -> Option<Url> {
150        let url_str = self.request.value()?.url.value()?;
151        Url::parse(url_str).ok()
152    }
153
154    fn user_agent(&self) -> UserAgent<'_> {
155        UserAgent {
156            raw: self.user_agent(),
157            parsed: None,
158        }
159    }
160
161    fn header(&self, header_name: &str) -> Option<&str> {
162        self.request
163            .value()?
164            .headers
165            .value()?
166            .get_header(header_name)
167    }
168}
169
170impl Filterable for Span {
171    fn ip_addr(&self) -> Option<&str> {
172        self.data.value()?.client_address.as_str()
173    }
174
175    fn release(&self) -> Option<&str> {
176        self.data.value()?.release.as_str()
177    }
178
179    fn transaction(&self) -> Option<&str> {
180        self.data.value()?.segment_name.as_str()
181    }
182
183    fn url(&self) -> Option<Url> {
184        let url_str = self.data.value()?.url_full.as_str()?;
185        Url::parse(url_str).ok()
186    }
187
188    fn user_agent(&self) -> UserAgent<'_> {
189        let raw = self
190            .data
191            .value()
192            .and_then(|data| data.user_agent_original.as_str());
193        UserAgent { raw, parsed: None }
194    }
195}
196
197impl Filterable for SpanV2 {
198    fn release(&self) -> Option<&str> {
199        self.attributes.value()?.get_value(RELEASE)?.as_str()
200    }
201
202    fn transaction(&self) -> Option<&str> {
203        self.attributes.value()?.get_value(SEGMENT_NAME)?.as_str()
204    }
205
206    fn user_agent(&self) -> UserAgent<'_> {
207        user_agent_from_attributes(&self.attributes)
208    }
209}
210
211impl Filterable for SessionUpdate {
212    fn ip_addr(&self) -> Option<&str> {
213        self.attributes
214            .ip_address
215            .as_ref()
216            .map(|addr| addr.as_str())
217    }
218
219    fn release(&self) -> Option<&str> {
220        Some(&self.attributes.release)
221    }
222
223    fn user_agent(&self) -> UserAgent<'_> {
224        UserAgent {
225            raw: self.attributes.user_agent.as_deref(),
226            parsed: None,
227        }
228    }
229}
230
231impl Filterable for SessionAggregates {
232    fn ip_addr(&self) -> Option<&str> {
233        self.attributes
234            .ip_address
235            .as_ref()
236            .map(|addr| addr.as_str())
237    }
238
239    fn release(&self) -> Option<&str> {
240        Some(&self.attributes.release)
241    }
242
243    fn user_agent(&self) -> UserAgent<'_> {
244        UserAgent {
245            raw: self.attributes.user_agent.as_deref(),
246            parsed: None,
247        }
248    }
249}
250
251impl Filterable for OurLog {
252    fn release(&self) -> Option<&str> {
253        self.attributes.value()?.get_value(RELEASE)?.as_str()
254    }
255
256    fn user_agent(&self) -> UserAgent<'_> {
257        user_agent_from_attributes(&self.attributes)
258    }
259}
260
261impl Filterable for TraceMetric {
262    fn release(&self) -> Option<&str> {
263        self.attributes.value()?.get_value(RELEASE)?.as_str()
264    }
265
266    fn user_agent(&self) -> UserAgent<'_> {
267        user_agent_from_attributes(&self.attributes)
268    }
269}
270
271fn user_agent_from_attributes(attributes: &relay_protocol::Annotated<Attributes>) -> UserAgent<'_> {
272    let parsed = (|| {
273        let attributes = attributes.value()?;
274
275        let family = attributes.get_value(BROWSER_NAME)?.as_str()?;
276        let version = attributes.get_value(BROWSER_VERSION)?.as_str()?;
277        let mut parts = version.splitn(3, '.');
278
279        Some(relay_ua::UserAgent {
280            family: family.into(),
281            major: parts.next().map(Into::into),
282            minor: parts.next().map(Into::into),
283            patch: parts.next().map(Into::into),
284        })
285    })();
286
287    UserAgent { raw: None, parsed }
288}