diff --git a/CHANGELOG.md b/CHANGELOG.md index f4ed36f9bd5..36e30c6d993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - Allow configuring Objectstore client auth parameters. ([#5720](https://github.com/getsentry/relay/pull/5720)) - Metric size limit per metric default changed to 1mib. ([#5779](https://github.com/getsentry/relay/pull/5779)) - Use `gen_ai.function_id` as a fallback for `gen_ai.agent.name`. ([#5776](https://github.com/getsentry/relay/pull/5776)) +- Extract `http.query` and `url.query` attributes from `query_string` in transactions' request context. ([#5784](https://github.com/getsentry/relay/pull/5784)) **Internal**: diff --git a/relay-event-schema/src/protocol/request.rs b/relay-event-schema/src/protocol/request.rs index d973b21ddf1..75aef170581 100644 --- a/relay-event-schema/src/protocol/request.rs +++ b/relay-event-schema/src/protocol/request.rs @@ -286,6 +286,23 @@ impl Query { form_urlencoded::parse(string.as_bytes()).collect() } + + /// Serializes the query pairs back to a URL-encoded query string without a leading `?`. + /// + /// Returns `None` if there are no valid pairs. + pub fn to_query_string(&self) -> Option { + let mut serializer = form_urlencoded::Serializer::new(String::new()); + let mut has_pairs = false; + for pair in self.iter() { + if let Some((key, value)) = pair.value() + && let (Some(k), Some(v)) = (key.as_str(), value.as_str()) + { + serializer.append_pair(k, v); + has_pairs = true; + } + } + has_pairs.then(|| serializer.finish()) + } } impl std::ops::Deref for Query { @@ -858,6 +875,47 @@ mod tests { assert_eq!(cookies, Annotated::from_json("42").unwrap()); } + #[test] + fn test_query_to_query_string() { + let query = Query( + vec![ + Annotated::new(( + Annotated::new("foo".to_owned()), + Annotated::new("bar".to_owned().into()), + )), + Annotated::new(( + Annotated::new("baz".to_owned()), + Annotated::new("qux".to_owned().into()), + )), + ] + .into(), + ); + + assert_eq!(query.to_query_string(), Some("foo=bar&baz=qux".to_owned())); + } + + #[test] + fn test_query_to_query_string_empty() { + let query = Query(PairList(vec![])); + assert_eq!(query.to_query_string(), None); + } + + #[test] + fn test_query_to_query_string_special_chars() { + let query = Query( + vec![Annotated::new(( + Annotated::new("q".to_owned()), + Annotated::new("hello world&more".to_owned().into()), + ))] + .into(), + ); + + assert_eq!( + query.to_query_string(), + Some("q=hello+world%26more".to_owned()) + ); + } + #[test] fn test_querystring_without_value() { let json = r#""foo=bar&baz""#; diff --git a/relay-event-schema/src/protocol/span.rs b/relay-event-schema/src/protocol/span.rs index 626daa51885..69b26e27581 100644 --- a/relay-event-schema/src/protocol/span.rs +++ b/relay-event-schema/src/protocol/span.rs @@ -968,6 +968,14 @@ pub struct SpanData { #[metastructure(field = "url.full")] pub url_full: Annotated, + /// The query string component of the URL, without a leading `?`. + #[metastructure(field = "url.query")] + pub url_query: Annotated, + + /// The query string component of the URL, with a leading `?`. + #[metastructure(field = "http.query")] + pub http_query: Annotated, + /// The client's IP address. #[metastructure(field = "client.address")] pub client_address: Annotated, @@ -1052,6 +1060,8 @@ impl Getter for SpanData { "thread\\.name" => self.thread_name.as_str()?.into(), "ui\\.component_name" => self.ui_component_name.value()?.into(), "url\\.scheme" => self.url_scheme.value()?.into(), + "url\\.query" => self.url_query.as_str()?.into(), + "http\\.query" => self.http_query.as_str()?.into(), "user" => self.user.value()?.into(), "user\\.email" => self.user_email.as_str()?.into(), "user\\.full_name" => self.user_full_name.as_str()?.into(), @@ -1612,6 +1622,8 @@ mod tests { messaging_operation_type: "create", user_agent_original: "Chrome", url_full: "my_url.com", + url_query: ~, + http_query: ~, client_address: IpAddr( "192.168.0.1", ), diff --git a/relay-event-schema/src/protocol/span/convert.rs b/relay-event-schema/src/protocol/span/convert.rs index 8f314a45b96..7110b869f98 100644 --- a/relay-event-schema/src/protocol/span/convert.rs +++ b/relay-event-schema/src/protocol/span/convert.rs @@ -40,6 +40,13 @@ impl From<&Event> for Span { span_data.sdk_name = client_sdk.name.clone(); span_data.sdk_version = client_sdk.version.clone(); } + if let Some(request) = event.request.value() + && let Some(query) = request.query_string.value() + && let Some(qs) = query.to_query_string() + { + span_data.http_query = format!("?{qs}").into(); + span_data.url_query = qs.into(); + } Self { timestamp: timestamp.clone(), @@ -121,6 +128,11 @@ mod tests { ] } }, + "request": { + "url": "http://example.com/api/0/organizations/", + "method": "GET", + "query_string": "project=1&sort=date" + }, "measurements": { "memory": { "value": 9001.0, @@ -255,6 +267,8 @@ mod tests { messaging_operation_type: ~, user_agent_original: ~, url_full: ~, + url_query: "project=1&sort=date", + http_query: "?project=1&sort=date", client_address: ~, route: ~, previous_route: ~,