Skip to content
Open
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 @@ -6,6 +6,7 @@

- Add metadata support for the `/upload` endpoint. ([#6028](https://github.com/getsentry/relay/pull/6028))
- Infer user agents and client addresses in the V2 standalone span pipeline. ([#6047](https://github.com/getsentry/relay/pull/6047))
- Equalize name and description for spans with "manual" origin. ([#6070](https://github.com/getsentry/relay/pull/6070))

**Bug Fixes**:

Expand Down
53 changes: 36 additions & 17 deletions relay-event-normalization/src/eap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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_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,33 @@ 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>,
) {
if attributes
.value()
.is_some_and(|attrs| attrs.contains_key(SENTRY__DESCRIPTION))
Comment on lines +63 to +64

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 might need a check which doesn't just use value() but also considers meta, Empty trait might be able to do that for you.

{
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Empty description blocks manual backfill

Medium Severity

normalize_sentry_description returns immediately when sentry.description is present, without treating an empty value as missing. For manual-origin spans with a non-empty name but an empty sentry.description attribute, description is never copied from name, so name and description can stay out of sync after processing.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 03de602. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's intended that if someone manually sets a description it doesn't get clobbered by the name. This includes the case of an empty description.


if let Some(description) = derive_description_for_v2_span(attributes, name) {
attributes
.get_or_insert_with(Default::default)
.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 +792,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 +804,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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Legacy action prefers DB over HTTP

Low Severity

In write_legacy_attributes, skipping when a legacy key already exists means sentry.action is filled from the first mapped conventional attribute and later mappings are ignored. When both db.operation.name and http.request.method are present, sentry.action stays on the DB value instead of being overwritten by the HTTP method as before.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 03de602. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The behavior for spans which have both db and http attributes (which should not happen in practice) was already arbitrary before, now it's arbitrary in the other direction.

}

if !attributes.contains_key(SENTRY__DOMAIN)
Expand All @@ -803,12 +832,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 +2272,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::backfill_description(&mut spans);

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.

Maybe process::normalize_derived or something similar?


match dynamic_sampling::try_split_indexed_and_total(spans, ctx) {
Either::Left(spans) => Ok(Output::just(SpanOutput::TotalAndIndexed(spans))),
Expand Down
18 changes: 12 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,18 @@ fn normalize_span(
Ok(())
}

pub fn backfill_description(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);
}
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Manual spans desync after scrub

Medium Severity

For manual-origin spans, backfill_description skips when sentry.description already exists, but PII scrubbing runs immediately before it and can change that attribute. Top-level span.name is not updated to match, so name and description can diverge after the step meant to equalize them.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e7c7306. Configure here.


/// 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 +940,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 +1001,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 +1120,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();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Trace origin applied to child spans

Medium Severity

When extracting transaction child spans, missing span origin is filled from the trace context before V1→V2 conversion. If the trace origin is manual, children without their own origin inherit it and later use manual name/description rules instead of convention-based naming.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 03de602. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct.


Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Transaction spans skip description backfill

Medium Severity

normalize_sentry_description runs only in the standalone span pipeline after PII scrubbing. Transaction-extracted spans are converted with span_v1_to_span_v2 and never get that step, so manual spans (including children that inherit trace manual origin) can keep a convention-inferred name while sentry.description stays unset.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 18d7a6f. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Backfilling the description from the name is not a concern for transaction-extracted spans, they need to worry about the converse.

// 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
40 changes: 40 additions & 0 deletions relay-spans/src/description.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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: &Annotated<Attributes>,
name: &Annotated<String>,
) -> Option<String> {
let attributes = attributes.value()?;

if attributes
.get_value(SENTRY__ORIGIN)
.and_then(|o| o.as_str())
== Some("manual")
Comment on lines +21 to +24

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();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Manual origin skips query backfill

Medium Severity

derive_description_for_v2_span returns the span name whenever sentry.origin is "manual" before considering db.query.text or HTTP attributes. Child spans that inherit a manual trace origin but lack their own origin can keep a convention-derived name, so description backfill copies that label instead of the DB query or HTTP URL that used to be written via write_legacy_attributes.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 61a998e. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I legitimately don't understand what this is trying to say.

@elramen elramen Jun 11, 2026

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.

I think it is trying to say "A child span might have name derived based on convention rules. Do we really want to use this derived name, instead of looking at DB__QUERY__TEXT etc., when the child span wasn't really manually instrumented (it just inherited manual)?"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Manual backfill skips PII scrub

Medium Severity

For "manual" origin, backfill_description runs after PII scrub and sets sentry.description from the top-level span.name. That value is not taken from already-scrubbed attributes, and the new attribute is never run through scrub again—unlike DB/HTTP backfill paths that read scrubbed attribute data.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7dbff4a. Configure here.


if let Some(&Value::String(db_query)) = attributes.get_value(DB__QUERY__TEXT).as_ref() {
return Some(db_query.clone());
}

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())
);
}
}
5 changes: 4 additions & 1 deletion tests/integration/test_spans.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ def test_span_extraction(
"attributes": {"span_key": {"type": "string", "value": "span_value"}},
},
],
"name": "http",
# This span has origin "manual", so the name should just be the description.
"name": "GET /api/0/organizations/?member=1",
"organization_id": 1,
"parent_span_id": "968cff94913ebb07",
"project_id": 42,
Expand Down Expand Up @@ -268,6 +269,8 @@ def test_span_extraction(
"attributes": {"txn_key": {"type": "integer", "value": 123}},
},
],
# This is a segment span, so its name should be the transaction
# (despite the origin being "manual").
"name": "hi",
"organization_id": 1,
"project_id": 42,
Expand Down
Loading
Loading