Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Infer user agents and client addresses in the V2 standalone span pipeline. ([#6047](https://github.com/getsentry/relay/pull/6047))
- Conditionally allow additional exceptions on minidumps. ([#6063](https://github.com/getsentry/relay/pull/6063))
- Replace nil Trace IDs with random ones. ([#6079](https://github.com/getsentry/relay/pull/6079))
- Equalize name and description for spans with "manual" origin. ([#6070](https://github.com/getsentry/relay/pull/6070))

**Bug Fixes**:

Expand Down
56 changes: 38 additions & 18 deletions relay-event-normalization/src/eap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use relay_common::time::UnixTimestamp;
use relay_conventions::attributes::*;
use relay_conventions::{AttributeInfo, ReplacementName, WriteBehavior};
use relay_event_schema::protocol::{Attribute, AttributeType, Attributes, BrowserContext, Geo};
use relay_protocol::{Annotated, Error, ErrorKind, Meta, Remark, RemarkType, Value};
use relay_spans::derive_op_for_v2_span;
use relay_protocol::{Annotated, Empty, Error, ErrorKind, Meta, Remark, RemarkType, Value};
use relay_spans::{derive_description_for_v2_span, derive_op_for_v2_span};

use crate::span::TABLE_NAME_REGEX;
use crate::span::description::{scrub_db_query, scrub_http};
Expand Down Expand Up @@ -46,6 +46,34 @@ pub fn normalize_sentry_op(attributes: &mut Annotated<Attributes>) {
attrs.insert_if_missing(SENTRY__OP, || inferred_op);
}

/// Normalizes a V2 span's [`SENTRY__DESCRIPTION`] attribute.
///
/// For now, this tries the following steps, in order:
/// - backfill from the span's name if its [`SENTRY__ORIGIN`] attribute is `"manual"`
/// - backfill from the span's [`DB__QUERY__TEXT`] attribute if it exists
/// - backfill a combination of the span's [`HTTP__REQUEST__METHOD`] and
/// [`URL__FULL`] attributes, if they both exists.
///
/// In the future, this logic will be partly moved to and extended in `sentry-conventions`.
pub fn normalize_sentry_description(
attributes: &mut Annotated<Attributes>,
name: &Annotated<String>,
) {
let Some(attributes) = attributes.value_mut() else {
return;
};

let description = attributes.get_annotated_value(SENTRY__DESCRIPTION);

if description.is_some_and(|d| !d.is_empty()) {
return;
}
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

if let Some(description) = derive_description_for_v2_span(attributes, name) {
attributes.insert(SENTRY__DESCRIPTION, description);
}
}

/// Infers the sentry.category attribute and inserts it into `attributes` if not
/// already set. The category is derived from the span operation or other span
/// attributes.
Expand Down Expand Up @@ -765,7 +793,6 @@ pub fn write_legacy_attributes(attributes: &mut Annotated<Attributes>) {
)]
let current_to_legacy_attributes = [
// DB attributes
(DB__QUERY__TEXT, SENTRY__DESCRIPTION),
(SENTRY__NORMALIZED_DB_QUERY, SENTRY__NORMALIZED_DESCRIPTION),
(DB__OPERATION__NAME, SENTRY__ACTION),
(DB__SYSTEM__NAME, DB__SYSTEM),
Expand All @@ -778,12 +805,15 @@ pub fn write_legacy_attributes(attributes: &mut Annotated<Attributes>) {
];

for (current_attribute, legacy_attribute) in current_to_legacy_attributes {
if attributes.contains_key(current_attribute) {
let Some(attr) = attributes.get_attribute(current_attribute) else {
continue;
};
attributes.insert(legacy_attribute, attr.value.clone());
if attributes.contains_key(legacy_attribute) {
continue;
}

let Some(attr) = attributes.get_attribute(current_attribute) else {
continue;
};

attributes.insert(legacy_attribute, attr.value.clone());
Comment thread
cursor[bot] marked this conversation as resolved.
}

if !attributes.contains_key(SENTRY__DOMAIN)
Expand All @@ -803,12 +833,6 @@ pub fn write_legacy_attributes(attributes: &mut Annotated<Attributes>) {
},
);
}

if let Some(&Value::String(method)) = attributes.get_value(HTTP__REQUEST__METHOD).as_ref()
&& let Some(&Value::String(url)) = attributes.get_value(URL__FULL).as_ref()
{
attributes.insert(SENTRY__DESCRIPTION, format!("{method} {url}"))
}
}

#[cfg(test)]
Expand Down Expand Up @@ -2249,10 +2273,6 @@ mod tests {
"type": "string",
"value": "FIND"
},
"sentry.description": {
"type": "string",
"value": "{\"find\": \"documents\", \"foo\": \"bar\"}"
},
"sentry.domain": {
"type": "string",
"value": ",documents,"
Expand Down
1 change: 1 addition & 0 deletions relay-server/src/processing/spans/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ impl processing::Processor for SpansProcessor {
};

process::scrub(&mut spans, ctx);
process::normalize_derived(&mut spans);

match dynamic_sampling::try_split_indexed_and_total(spans, ctx) {
Either::Left(spans) => Ok(Output::just(SpanOutput::TotalAndIndexed(spans))),
Expand Down
22 changes: 16 additions & 6 deletions relay-server/src/processing/spans/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,22 @@ fn normalize_span(
Ok(())
}

/// Normalize derived fields and attributes.
///
/// This is separate from [`normalize`] because it needs to run
/// after PII scrubbing; PII might get leaked otherwise.
pub fn normalize_derived(spans: &mut Managed<ExpandedSpans>) {
spans.modify(|span, _| {
for span in &mut span.spans {
let Some(span) = span.span.value_mut() else {
continue;
};
Comment thread
cursor[bot] marked this conversation as resolved.

eap::normalize_sentry_description(&mut span.attributes, &span.name);
}
});
}
Comment thread
cursor[bot] marked this conversation as resolved.

/// Validates the start and end timestamps of a span.
///
/// The start timestamp must not be after the end timestamp.
Expand Down Expand Up @@ -928,7 +944,6 @@ mod tests {
assert_attributes_contains(
&span,
&[
(SENTRY__DESCRIPTION, "select * from users where id = 1"),
(
SENTRY__NORMALIZED_DESCRIPTION,
"SELECT * FROM users WHERE id = %s",
Expand Down Expand Up @@ -990,10 +1005,6 @@ mod tests {
&[
(SENTRY__CATEGORY, "http"),
(SENTRY__OP, "http.client"),
(
SENTRY__DESCRIPTION,
"GET https://www.example.com/path?param=value",
),
(SENTRY__ACTION, "GET"),
(SENTRY__DOMAIN, "*.example.com"),
],
Expand Down Expand Up @@ -1113,7 +1124,6 @@ mod tests {
&span,
&[
(SENTRY__OP, "db"),
(SENTRY__DESCRIPTION, "select * from users where id = 1"),
(
SENTRY__NORMALIZED_DESCRIPTION,
"SELECT * FROM users WHERE id = %s",
Expand Down
11 changes: 10 additions & 1 deletion relay-server/src/processing/transactions/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::processing;
use crate::processing::utils::event::event_type;
use relay_base_schema::events::EventType;
use relay_config::Config;
use relay_event_schema::protocol::{Event, Measurement, Measurements, Span, SpanV2};
use relay_event_schema::protocol::{Event, Measurement, Measurements, Span, SpanV2, TraceContext};
use relay_metrics::MetricNamespace;
use relay_metrics::{FractionUnit, MetricUnit};
use relay_protocol::{Annotated, Empty};
Expand Down Expand Up @@ -41,6 +41,11 @@ pub fn extract_from_event(

// Add child spans.
if let Some(spans) = event.spans.value() {
let origin = event
.context::<TraceContext>()
.map(|trace| trace.origin.clone())
.unwrap_or_default();

for span in spans {
let Some(inner_span) = span.value() else {
continue;
Expand All @@ -54,6 +59,10 @@ pub fn extract_from_event(
new_span.segment_id = transaction_span.segment_id.clone();
new_span.platform = transaction_span.platform.clone();

if new_span.origin.value().is_none() {
new_span.origin = origin.clone();
}
Comment thread
cursor[bot] marked this conversation as resolved.

Comment thread
cursor[bot] marked this conversation as resolved.
// If a profile is associated with the transaction, also associate it with its
// child spans.
new_span.profile_id = transaction_span.profile_id.clone();
Expand Down
38 changes: 38 additions & 0 deletions relay-spans/src/description.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use relay_conventions::attributes::*;
use relay_event_schema::protocol::Attributes;
use relay_protocol::{Annotated, Value};

/// Derives a description for a V2 span, based on its name
/// and attributes.
///
/// For now, this tries the following steps, in order:
/// - returns the span's name if its [`SENTRY__ORIGIN`] attribute is `"manual"`
/// - returns the span's [`DB__QUERY__TEXT`] attribute if it exists
/// - returns a combination of the span's [`HTTP__REQUEST__METHOD`] and
/// [`URL__FULL`] attributes, if they both exists.
///
/// In the future, this logic will be partly moved to and extended in `sentry-conventions`.
pub fn derive_description_for_v2_span(
attributes: &Attributes,
name: &Annotated<String>,
) -> Option<String> {
if attributes
.get_value(SENTRY__ORIGIN)
.and_then(|o| o.as_str())
== Some("manual")
Comment on lines +19 to +22

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This formatting is weird af

{
return name.value().cloned();
}
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

if let Some(&Value::String(db_query)) = attributes.get_value(DB__QUERY__TEXT).as_ref() {
return Some(db_query.clone());
}
Comment on lines +27 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Moving sentry.description population to after PII scrubbing bypasses custom scrubbing rules for that field, potentially leaking sensitive data from DB queries or URLs.
Severity: HIGH

Suggested Fix

Ensure that derived span descriptions are generated before the PII scrubbing process runs. This would involve calling normalize_derived() before process::scrub(), restoring the previous behavior where custom PII rules targeting sentry.description can effectively remove sensitive information.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: relay-spans/src/description.rs#L27-L29

Potential issue: For non-manual V2 spans, such as database or HTTP spans, the
`sentry.description` attribute is now populated after PII scrubbing has already run.
Previously, this attribute was populated before scrubbing, allowing custom PII rules
targeting `sentry.description` to remove sensitive data like database query text or URL
parameters. Because the attribute is now absent during the scrubbing process, these
custom rules are no longer effective. This results in potentially sensitive information,
which was previously scrubbed, being included in the `sentry.description` field.


if let Some(&Value::String(method)) = attributes.get_value(HTTP__REQUEST__METHOD).as_ref()
&& let Some(&Value::String(url)) = attributes.get_value(URL__FULL).as_ref()
{
return Some(format!("{method} {url}"));
}

None
}
2 changes: 2 additions & 0 deletions relay-spans/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
html_favicon_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png"
)]

pub use crate::description::derive_description_for_v2_span;
pub use crate::name::name_for_span;
pub use crate::op::derive_op_for_v2_span;
pub use crate::otel_to_sentry_v2::otel_to_sentry_span as otel_to_sentry_span_v2;
pub use crate::v1_to_v2::span_v1_to_span_v2;

pub use opentelemetry_proto::tonic::trace::v1 as otel_trace;

mod description;
mod name;
mod op;
mod otel_to_sentry_v2;
Expand Down
105 changes: 102 additions & 3 deletions relay-spans/src/name.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
use relay_conventions::attributes::SENTRY__OP;
use relay_conventions::attributes::{SENTRY__DESCRIPTION, SENTRY__OP, SENTRY__ORIGIN};
use relay_conventions::name_for_op_and_attributes;
use relay_event_schema::protocol::{Attributes, Span};
use relay_protocol::{Getter, GetterIter, Val};

/// Constructs a name attribute for a span, following the rules defined in sentry-conventions.
/// Constructs a name attribute for a V1 span.
///
/// If the span's origin is `"manual"`, its description is used as the name.
/// Otherwise, the name is constructed following the rules defined in sentry-conventions.
pub fn name_for_span(span: &Span) -> Option<String> {
let origin = span.origin.value().map(|o| o.as_str());
let description = span.description.value().map(|d| d.as_str());

if let Some(name) = name_from_origin_and_description(origin, description) {
return Some(name);
}

let op = span.op.value()?;

let Some(data) = span.data.value() else {
Expand All @@ -20,12 +30,38 @@ pub fn name_for_span(span: &Span) -> Option<String> {
))
}

/// Constructs a name attribute for a span, following the rules defined in sentry-conventions.
/// Constructs a name attribute for a V2 span, based on its attributes.
///
/// If the attributes contain [`SENTRY__ORIGIN`] with the value `"manual"`,
/// the description (contained in [`SENTRY__DESCRIPTION`]) is used as the name.
/// Otherwise, the name is constructed following the rules defined in sentry-conventions.
pub fn name_for_attributes(attributes: &Attributes) -> Option<String> {
let origin = attributes
.get_value(SENTRY__ORIGIN)
.and_then(|o| o.as_str());
let description = attributes
.get_value(SENTRY__DESCRIPTION)
.and_then(|d| d.as_str());

if let Some(name) = name_from_origin_and_description(origin, description) {
return Some(name);
}

let op = attributes.get_value(SENTRY__OP)?.as_str()?;
Some(name_for_op_and_attributes(op, &AttributeGetter(attributes)))
}

fn name_from_origin_and_description(
origin: Option<&str>,
description: Option<&str>,
) -> Option<String> {
if origin == Some("manual") {
description.map(String::from)
} else {
None
}
}

struct EmptyGetter {}

impl Getter for EmptyGetter {
Expand Down Expand Up @@ -203,4 +239,67 @@ mod tests {
Some("Database operation".to_owned())
);
}

#[test]
fn test_manual_spans_use_description_v1() {
let span = Span {
origin: Annotated::new("manual".to_owned()),
description: Annotated::new("Custom name".to_owned()),
op: Annotated::new("db".to_owned()),
data: Annotated::new(SpanData {
other: Object::from([
(
"db.query.summary".to_owned(),
Value::String("SELECT users".to_owned()).into(),
),
(
"db.operation.name".to_owned(),
Value::String("INSERT".to_owned()).into(),
),
(
"db.collection.name".to_owned(),
Value::String("widgets".to_owned()).into(),
),
]),
..Default::default()
}),
..Default::default()
};
assert_eq!(name_for_span(&span), Some("Custom name".to_owned()));
}

#[test]
fn test_manual_spans_use_description_v2() {
let attributes = Attributes::from([
(
"sentry.origin".to_owned(),
Annotated::new("manual".to_owned().into()),
),
(
"sentry.description".to_owned(),
Annotated::new("Custom name".to_owned().into()),
),
(
"sentry.op".to_owned(),
Annotated::new("db".to_owned().into()),
),
(
"db.query.summary".to_owned(),
Annotated::new("SELECT users".to_owned().into()),
),
(
"db.operation.name".to_owned(),
Annotated::new("INSERT".to_owned().into()),
),
(
"db.collection.name".to_owned(),
Annotated::new("widgets".to_owned().into()),
),
]);

assert_eq!(
name_for_attributes(&attributes),
Some("Custom name".to_owned())
);
}
}
Loading
Loading