Skip to main content

relay_event_normalization/normalize/
utils.rs

1//! **Deprecated.** Utilities for extracting common event fields.
2//!
3//! This utility module is being phased out. Functionality in this module should be moved to the
4//! specific normalization file requiring this data access.
5
6use std::f64::consts::SQRT_2;
7
8use relay_conventions::attributes::HTTP__RESPONSE__STATUS_CODE;
9use relay_event_schema::protocol::{Event, ResponseContext, Span, TraceContext, User};
10use relay_protocol::Value;
11
12/// Used to decide when to extract mobile-specific tags.
13pub const MOBILE_SDKS: [&str; 4] = [
14    "sentry.cocoa",
15    "sentry.dart.flutter",
16    "sentry.java.android",
17    "sentry.javascript.react-native",
18];
19
20/// Allowed value for main thread name.
21pub const MAIN_THREAD_NAME: &str = "main";
22
23/// Maximum duration of a mobile measurement in milliseconds.
24///
25/// Mobile measurements (app start, TTID, TTFD) that exceed this threshold are considered
26/// outliers and removed.
27pub const MAX_DURATION_MOBILE_MS: f64 = 180_000.0;
28
29/// Extract the HTTP status code from the span data.
30pub fn http_status_code_from_span(span: &Span) -> Option<String> {
31    // For SDKs which put the HTTP status code into the span data.
32    if let Some(status_code) = span
33        .data
34        .value()
35        .and_then(|data| data.get_value(HTTP__RESPONSE__STATUS_CODE))
36        .map(|v| match v {
37            Value::String(s) => Some(s.as_str().to_owned()),
38            Value::I64(i) => Some(i.to_string()),
39            Value::U64(u) => Some(u.to_string()),
40            _ => None,
41        })
42    {
43        return status_code;
44    }
45
46    // For SDKs which put the HTTP status code into the span tags.
47    if let Some(status_code) = span
48        .tags
49        .value()
50        .and_then(|tags| tags.get("http.status_code"))
51        .and_then(|v| v.as_str())
52        .map(|v| v.to_owned())
53    {
54        return Some(status_code);
55    }
56
57    None
58}
59
60/// Extracts the HTTP status code.
61pub fn extract_http_status_code(event: &Event) -> Option<String> {
62    // For SDKs which put the HTTP status code in the event tags.
63    if let Some(status_code) = event.tag_value("http.status_code") {
64        return Some(status_code.to_owned());
65    }
66
67    if let Some(spans) = event.spans.value() {
68        for span in spans {
69            if let Some(span_value) = span.value()
70                && let Some(status_code) = http_status_code_from_span(span_value)
71            {
72                return Some(status_code);
73            }
74        }
75    }
76
77    // For SDKs which put the HTTP status code into the breadcrumbs data.
78    if let Some(breadcrumbs) = event.breadcrumbs.value()
79        && let Some(values) = breadcrumbs.values.value()
80    {
81        for breadcrumb in values {
82            // We need only the `http` type.
83            if let Some(crumb) = breadcrumb
84                .value()
85                .filter(|bc| bc.ty.as_str() == Some("http"))
86            {
87                // Try to get the status code om the map.
88                if let Some(status_code) = crumb.data.value().and_then(|v| v.get("status_code")) {
89                    return status_code.value().and_then(|v| v.as_str()).map(Into::into);
90                }
91            }
92        }
93    }
94
95    // For SDKs which put the HTTP status code in the `Response` context.
96    if let Some(response_context) = event.context::<ResponseContext>() {
97        let status_code = response_context
98            .status_code
99            .value()
100            .map(|code| code.to_string());
101        return status_code;
102    }
103
104    None
105}
106
107/// Compute the transaction event's "user" tag as close as possible to how users are determined in
108/// the transactions dataset in Snuba. This should produce the exact same user counts as the `user`
109/// column in Discover for Transactions, barring:
110///
111/// * imprecision caused by HLL sketching in Snuba, which we don't have in events
112/// * hash collisions in `BucketValue::set_from_display`, which we don't have in events
113/// * MD5-collisions caused by `EventUser.hash_from_tag`, which we don't have in metrics
114///
115///   MD5 is used to efficiently look up the current event user for an event, and if there is a
116///   collision it seems that this code will fetch an event user with potentially different values
117///   for everything that is in `defaults`:
118///   <https://github.com/getsentry/sentry/blob/f621cd76da3a39836f34802ba9b35133bdfbe38b/src/sentry/event_manager.py#L1058-L1060>
119///
120/// The performance product runs a discover query such as `count_unique(user)`, which maps to two
121/// things:
122///
123/// * `user` metric for the metrics dataset
124/// * the "promoted tag" column `user` in the transactions clickhouse table
125///
126/// A promoted tag is a tag that snuba pulls out into its own column. In this case it pulls out the
127/// `sentry:user` tag from the event payload:
128/// <https://github.com/getsentry/snuba/blob/430763e67e30957c89126e62127e34051eb52fd6/snuba/datasets/transactions_processor.py#L151>
129///
130/// Sentry's processing pipeline defers to `sentry.models.EventUser` to produce the `sentry:user` tag
131/// here: <https://github.com/getsentry/sentry/blob/f621cd76da3a39836f34802ba9b35133bdfbe38b/src/sentry/event_manager.py#L790-L794>
132///
133/// `sentry.models.eventuser.KEYWORD_MAP` determines which attributes are looked up in which order, here:
134/// <https://github.com/getsentry/sentry/blob/f621cd76da3a39836f34802ba9b35133bdfbe38b/src/sentry/models/eventuser.py#L18>
135/// If its order is changed, this function needs to be changed.
136pub fn get_event_user_tag(user: &User) -> Option<String> {
137    if let Some(id) = user.id.as_str() {
138        return Some(format!("id:{id}"));
139    }
140
141    if let Some(username) = user.username.as_str() {
142        return Some(format!("username:{username}"));
143    }
144
145    if let Some(email) = user.email.as_str() {
146        return Some(format!("email:{email}"));
147    }
148
149    if let Some(ip_address) = user.ip_address.as_str() {
150        return Some(format!("ip:{ip_address}"));
151    }
152
153    None
154}
155
156/// Returns a normalized `op` from the given trace context.
157pub fn extract_transaction_op(trace_context: &TraceContext) -> Option<String> {
158    let op = trace_context.op.value()?;
159    if op == "default" {
160        // This was likely set by normalization, so let's treat it as None
161        // See https://github.com/getsentry/relay/blob/bb2ac4ee82c25faa07a6d078f93d22d799cfc5d1/relay-general/src/store/transactions.rs#L96
162
163        // Note that this is the opposite behavior of what we do for transaction.status, where
164        // we coalesce None to "unknown".
165        return None;
166    }
167    Some(op.to_string())
168}
169
170/// The Gauss error function.
171///
172/// See <https://en.wikipedia.org/wiki/Error_function>.
173fn erf(x: f64) -> f64 {
174    // constants
175    let a1 = 0.254829592;
176    let a2 = -0.284496736;
177    let a3 = 1.421413741;
178    let a4 = -1.453152027;
179    let a5 = 1.061405429;
180    let p = 0.3275911;
181    // Save the sign of x
182    let sign = if x < 0.0 { -1.0 } else { 1.0 };
183    let x = x.abs();
184    // A&S formula 7.1.26
185    let t = 1.0 / (1.0 + p * x);
186    let y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * (-x * x).exp();
187    sign * y
188}
189
190/// Sigma function for CDF score calculation.
191fn calculate_cdf_sigma(p10: f64, p50: f64) -> f64 {
192    (p10.ln() - p50.ln()).abs() / (SQRT_2 * 0.9061938024368232)
193}
194
195/// Computes the [cumulative distribution function](https://en.wikipedia.org/wiki/Cumulative_distribution_function)
196/// of a [log-normal distribution](https://en.wikipedia.org/wiki/Log-normal_distribution) with the given p10 and p50.
197///
198/// In other words, if `X` is log-normally distributed with 10th and 50th percentile `p10` and `p50`,
199/// then `calculate_cdf_score(x, p10, p50) = P(X ≤ x)`.
200pub fn calculate_cdf_score(value: f64, p10: f64, p50: f64) -> f64 {
201    0.5 * (1.0 - erf((f64::ln(value) - f64::ln(p50)) / (SQRT_2 * calculate_cdf_sigma(p50, p10))))
202}
203
204#[cfg(test)]
205mod tests {
206    use crate::utils::{get_event_user_tag, http_status_code_from_span};
207    use relay_event_schema::protocol::{Span, User};
208    use relay_protocol::Annotated;
209
210    #[test]
211    fn test_get_event_user_tag() {
212        // Note: If this order changes,
213        // https://github.com/getsentry/sentry/blob/f621cd76da3a39836f34802ba9b35133bdfbe38b/src/sentry/models/eventuser.py#L18
214        // has to be changed. Though it is probably not a good idea!
215        let user = User {
216            id: Annotated::new("ident".to_owned().into()),
217            username: Annotated::new("username".to_owned().into()),
218            email: Annotated::new("email".to_owned()),
219            ip_address: Annotated::new("127.0.0.1".parse().unwrap()),
220            ..User::default()
221        };
222
223        assert_eq!(get_event_user_tag(&user).unwrap(), "id:ident");
224
225        let user = User {
226            username: Annotated::new("username".to_owned().into()),
227            email: Annotated::new("email".to_owned()),
228            ip_address: Annotated::new("127.0.0.1".parse().unwrap()),
229            ..User::default()
230        };
231
232        assert_eq!(get_event_user_tag(&user).unwrap(), "username:username");
233
234        let user = User {
235            email: Annotated::new("email".to_owned()),
236            ip_address: Annotated::new("127.0.0.1".parse().unwrap()),
237            ..User::default()
238        };
239
240        assert_eq!(get_event_user_tag(&user).unwrap(), "email:email");
241
242        let user = User {
243            ip_address: Annotated::new("127.0.0.1".parse().unwrap()),
244            ..User::default()
245        };
246
247        assert_eq!(get_event_user_tag(&user).unwrap(), "ip:127.0.0.1");
248
249        let user = User::default();
250
251        assert!(get_event_user_tag(&user).is_none());
252    }
253
254    #[test]
255    fn test_extracts_http_status_code_when_int() {
256        let span = Annotated::<Span>::from_json(
257            r#"{
258                "data": {
259                    "http.response.status_code": 400
260                }
261            }"#,
262        )
263        .unwrap()
264        .into_value()
265        .unwrap();
266
267        let result = http_status_code_from_span(&span);
268
269        assert_eq!(result, Some("400".to_owned()));
270    }
271}