From f6503ee4af977cb8fa28973ae698edab51c1b7da Mon Sep 17 00:00:00 2001 From: Matt Quinn Date: Fri, 29 May 2026 11:33:42 -0400 Subject: [PATCH] fix(spans): Copy user Sentry tags into conventional attributes --- CHANGELOG.md | 4 ++ relay-spans/src/v1_to_v2.rs | 100 +++++++++++++++++++++++++++++++- tests/integration/test_spans.py | 28 ++++++++- 3 files changed, 130 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0ba5fd11c1..55dda1fb3d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Implement mobile measurements calculation for V2 spans. ([#6022](https://github.com/getsentry/relay/pull/6022)) +**Bug Fixes**: + +- Copy user Sentry tags into conventional attributes. ([#6030](https://github.com/getsentry/relay/pull/6030)) + **Internal**: - Forwards extracted transaction spans directly to Kafka instead of serializing to an intermediate envelope first. ([#6029](https://github.com/getsentry/relay/pull/6029)) diff --git a/relay-spans/src/v1_to_v2.rs b/relay-spans/src/v1_to_v2.rs index 346281c5e3c..124d71d4d65 100644 --- a/relay-spans/src/v1_to_v2.rs +++ b/relay-spans/src/v1_to_v2.rs @@ -100,11 +100,32 @@ pub fn span_v1_to_span_v2(span_v1: SpanV1, use_measurements_smart_conversion: bo && let Value::Object(tags) = tags.into_value() { for (key, value) in tags { + if value.is_empty() { + continue; + } + let conventional_key = match key.as_str() { + "user.email" => Some(USER__EMAIL), + "user.geo.city" => Some(USER__GEO__CITY), + "user.geo.country_code" => Some(USER__GEO__COUNTRY_CODE), + "user.geo.region" => Some(USER__GEO__REGION), + "user.geo.subdivision" => Some(USER__GEO__SUBDIVISION), + "user.id" => Some(USER__ID), + "user.ip" => Some(USER__IP_ADDRESS), + "user.username" => Some(USER__NAME), + _ => None, + }; + if let Some(conv_key) = conventional_key + && !attributes.contains_key(conv_key) + { + attributes + .0 + .insert(conv_key.to_owned(), attribute_from_value(value.clone())); + } let key = match key.as_str() { "description" => SENTRY__NORMALIZED_DESCRIPTION.into(), other => Cow::Owned(format!("sentry.{}", other)), }; - if !value.is_empty() && !attributes.contains_key(key.as_ref()) { + if !attributes.contains_key(key.as_ref()) { attributes .0 .insert(key.into_owned(), attribute_from_value(value)); @@ -312,6 +333,15 @@ mod tests { "sentry_tags": { "description": "normalized description", "user": "id:user123", + "user.email": "john@example.com", + "user.geo.city": "Vienna", + "user.geo.country_code": "AT", + "user.geo.region": "Europe", + "user.geo.subdivision": "AT-9", + "user.geo.subregion": "155", + "user.id": "user123", + "user.ip": "127.0.0.1", + "user.username": "john", }, "op": "operation", "origin": "auto.http", @@ -435,6 +465,74 @@ mod tests { "sentry.user": { "type": "string", "value": "id:user123" + }, + "sentry.user.email": { + "type": "string", + "value": "john@example.com" + }, + "sentry.user.geo.city": { + "type": "string", + "value": "Vienna" + }, + "sentry.user.geo.country_code": { + "type": "string", + "value": "AT" + }, + "sentry.user.geo.region": { + "type": "string", + "value": "Europe" + }, + "sentry.user.geo.subdivision": { + "type": "string", + "value": "AT-9" + }, + "sentry.user.geo.subregion": { + "type": "string", + "value": "155" + }, + "sentry.user.id": { + "type": "string", + "value": "user123" + }, + "sentry.user.ip": { + "type": "string", + "value": "127.0.0.1" + }, + "sentry.user.username": { + "type": "string", + "value": "john" + }, + "user.email": { + "type": "string", + "value": "john@example.com" + }, + "user.geo.city": { + "type": "string", + "value": "Vienna" + }, + "user.geo.country_code": { + "type": "string", + "value": "AT" + }, + "user.geo.region": { + "type": "string", + "value": "Europe" + }, + "user.geo.subdivision": { + "type": "string", + "value": "AT-9" + }, + "user.id": { + "type": "string", + "value": "user123" + }, + "user.ip_address": { + "type": "string", + "value": "127.0.0.1" + }, + "user.name": { + "type": "string", + "value": "john" } }, "_meta": { diff --git a/tests/integration/test_spans.py b/tests/integration/test_spans.py index 98eca91b903..4af50257003 100644 --- a/tests/integration/test_spans.py +++ b/tests/integration/test_spans.py @@ -155,6 +155,12 @@ def test_span_extraction( "sentry.user.geo.subregion": {"type": "string", "value": "155"}, "sentry.user.id": {"type": "string", "value": user_id}, "sentry.user.ip": {"type": "string", "value": "192.168.0.1"}, + "user.geo.city": {"type": "string", "value": "Vienna"}, + "user.geo.country_code": {"type": "string", "value": "AT"}, + "user.geo.region": {"type": "string", "value": "Austria"}, + "user.geo.subdivision": {"type": "string", "value": "Vienna"}, + "user.id": {"type": "string", "value": user_id}, + "user.ip_address": {"type": "string", "value": "192.168.0.1"}, "sentry.description": { "type": "string", "value": "GET /api/0/organizations/?member=1", @@ -234,6 +240,7 @@ def test_span_extraction( "sentry.trace.status": {"type": "string", "value": "ok"}, "sentry.transaction.op": {"type": "string", "value": "hi"}, "sentry.transaction": {"type": "string", "value": "hi"}, + "sentry.user": {"type": "string", "value": f"id:{user_id}"}, "sentry.user.geo.city": {"type": "string", "value": "Vienna"}, "sentry.user.geo.country_code": {"type": "string", "value": "AT"}, "sentry.user.geo.region": {"type": "string", "value": "Austria"}, @@ -241,7 +248,12 @@ def test_span_extraction( "sentry.user.geo.subregion": {"type": "string", "value": "155"}, "sentry.user.id": {"type": "string", "value": user_id}, "sentry.user.ip": {"type": "string", "value": "192.168.0.1"}, - "sentry.user": {"type": "string", "value": f"id:{user_id}"}, + "user.geo.city": {"type": "string", "value": "Vienna"}, + "user.geo.country_code": {"type": "string", "value": "AT"}, + "user.geo.region": {"type": "string", "value": "Austria"}, + "user.geo.subdivision": {"type": "string", "value": "Vienna"}, + "user.id": {"type": "string", "value": user_id}, + "user.ip_address": {"type": "string", "value": "192.168.0.1"}, "sentry.dsc.project_id": {"type": "string", "value": "42"}, "sentry.dsc.trace_id": { "type": "string", @@ -1165,11 +1177,21 @@ def test_scrubs_ip_addresses( child_span = spans_consumer.get_span() + assert child_span["_meta"]["attributes"]["user.email"] == { + "": {"len": 15, "rem": [["@email", "s", 0, 7]]} + } assert child_span["_meta"]["attributes"]["sentry.user.email"] == { "": {"len": 15, "rem": [["@email", "s", 0, 7]]} } if scrub_ip_addresses: + assert child_span["attributes"]["user.ip_address"] is None + assert child_span["_meta"]["attributes"]["user.ip_address"] == { + "": { + "len": 9, + "rem": [["@ip:replace", "s", 0, 4], ["@anything:remove", "x"]], + } + } assert child_span["attributes"]["sentry.user.ip"] is None assert child_span["_meta"]["attributes"]["sentry.user.ip"] == { "": { @@ -1178,14 +1200,18 @@ def test_scrubs_ip_addresses( } } else: + assert child_span["attributes"]["user.ip_address"]["value"] == "127.0.0.1" + assert "user.ip_address" not in child_span["_meta"]["attributes"] assert child_span["attributes"]["sentry.user.ip"]["value"] == "127.0.0.1" assert "sentry.user.ip" not in child_span["_meta"]["attributes"] parent_span = spans_consumer.get_span() if scrub_ip_addresses: + assert "user.ip_address" not in parent_span["attributes"] assert "sentry.user.ip" not in parent_span["attributes"] else: + assert parent_span["attributes"]["user.ip_address"]["value"] == "127.0.0.1" assert parent_span["attributes"]["sentry.user.ip"]["value"] == "127.0.0.1" spans_consumer.assert_empty()