From 2eae5a135d24fb89a42ec13d026faf91530eb919 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Tue, 9 Jun 2026 15:53:55 +0200 Subject: [PATCH 1/2] Use correct attribute names in normalize_mobile_attributes --- relay-event-normalization/src/eap/mobile.rs | 36 ++++++++++--------- ...bile__tests__outlier_keeps_start_cold.snap | 8 +++++ ...bile__tests__outlier_keeps_start_warm.snap | 8 +++++ relay-event-normalization/src/event.rs | 4 +-- .../src/normalize/span/tag_extraction.rs | 5 +-- relay-server/src/processing/spans/process.rs | 2 +- tests/integration/test_spans_standalone.py | 11 ++++++ 7 files changed, 53 insertions(+), 21 deletions(-) diff --git a/relay-event-normalization/src/eap/mobile.rs b/relay-event-normalization/src/eap/mobile.rs index a8fbd1ca60b..c249b4bd8ce 100644 --- a/relay-event-normalization/src/eap/mobile.rs +++ b/relay-event-normalization/src/eap/mobile.rs @@ -48,15 +48,19 @@ pub fn normalize_mobile_attributes(attributes: &mut Annotated) { // Normalize app start measurements into unified attributes. // V1 spans have measurements `app_start_cold`/`app_start_warm` which become - // attributes with those names after v1→v2 conversion. - // V2 spans will at some point send `app.vitals.start.value` + `app.vitals.start.type` directly. + // attributes `app.vitals.start.cold.value` and `app.vitals.start.warm.value`, respectively, + // after v1→v2 conversion. V2 spans will at some point send `app.vitals.start.value` + `app.vitals.start.type` directly. if !attrs.contains_key(APP__VITALS__START__VALUE) { - if let Some(value) = attrs.get_value("app_start_cold").and_then(|v| v.as_f64()) + if let Some(value) = attrs + .get_value(APP__VITALS__START__COLD__VALUE) + .and_then(|v| v.as_f64()) && value <= MAX_DURATION_MOBILE_MS { attrs.insert(APP__VITALS__START__VALUE, value); attrs.insert_if_missing(APP__VITALS__START__TYPE, || "cold".to_owned()); - } else if let Some(value) = attrs.get_value("app_start_warm").and_then(|v| v.as_f64()) + } else if let Some(value) = attrs + .get_value(APP__VITALS__START__WARM__VALUE) + .and_then(|v| v.as_f64()) && value <= MAX_DURATION_MOBILE_MS { attrs.insert(APP__VITALS__START__VALUE, value); @@ -315,13 +319,17 @@ mod tests { #[test] fn test_app_start_cold_normalized() { let mut attributes = Annotated::new(attributes! { - "app_start_cold" => 1234.0, + "app.vitals.start.cold.value" => 1234.0, }); normalize_mobile_attributes(&mut attributes); assert_annotated_snapshot!(attributes, @r#" { + "app.vitals.start.cold.value": { + "type": "double", + "value": 1234.0 + }, "app.vitals.start.type": { "type": "string", "value": "cold" @@ -329,10 +337,6 @@ mod tests { "app.vitals.start.value": { "type": "double", "value": 1234.0 - }, - "app_start_cold": { - "type": "double", - "value": 1234.0 } } "#); @@ -341,7 +345,7 @@ mod tests { #[test] fn test_app_start_warm_normalized() { let mut attributes = Annotated::new(attributes! { - "app_start_warm" => 567.0, + "app.vitals.start.warm.value" => 567.0, }); normalize_mobile_attributes(&mut attributes); @@ -356,7 +360,7 @@ mod tests { "type": "double", "value": 567.0 }, - "app_start_warm": { + "app.vitals.start.warm.value": { "type": "double", "value": 567.0 } @@ -369,13 +373,17 @@ mod tests { let mut attributes = Annotated::new(attributes! { APP__VITALS__START__VALUE => 999.0, APP__VITALS__START__TYPE => "warm", - "app_start_cold" => 1234.0, + "app.vitals.start.cold.value" => 1234.0, }); normalize_mobile_attributes(&mut attributes); assert_annotated_snapshot!(attributes, @r#" { + "app.vitals.start.cold.value": { + "type": "double", + "value": 1234.0 + }, "app.vitals.start.type": { "type": "string", "value": "warm" @@ -383,10 +391,6 @@ mod tests { "app.vitals.start.value": { "type": "double", "value": 999.0 - }, - "app_start_cold": { - "type": "double", - "value": 1234.0 } } "#); diff --git a/relay-event-normalization/src/eap/snapshots/relay_event_normalization__eap__mobile__tests__outlier_keeps_start_cold.snap b/relay-event-normalization/src/eap/snapshots/relay_event_normalization__eap__mobile__tests__outlier_keeps_start_cold.snap index e9420097fad..5cd2c1c95ed 100644 --- a/relay-event-normalization/src/eap/snapshots/relay_event_normalization__eap__mobile__tests__outlier_keeps_start_cold.snap +++ b/relay-event-normalization/src/eap/snapshots/relay_event_normalization__eap__mobile__tests__outlier_keeps_start_cold.snap @@ -6,5 +6,13 @@ expression: attributes "app.vitals.start.cold.value": { "type": "double", "value": 5000.0 + }, + "app.vitals.start.type": { + "type": "string", + "value": "cold" + }, + "app.vitals.start.value": { + "type": "double", + "value": 5000.0 } } diff --git a/relay-event-normalization/src/eap/snapshots/relay_event_normalization__eap__mobile__tests__outlier_keeps_start_warm.snap b/relay-event-normalization/src/eap/snapshots/relay_event_normalization__eap__mobile__tests__outlier_keeps_start_warm.snap index bbdaf4d2162..fed61e15823 100644 --- a/relay-event-normalization/src/eap/snapshots/relay_event_normalization__eap__mobile__tests__outlier_keeps_start_warm.snap +++ b/relay-event-normalization/src/eap/snapshots/relay_event_normalization__eap__mobile__tests__outlier_keeps_start_warm.snap @@ -3,6 +3,14 @@ source: relay-event-normalization/src/eap/mobile.rs expression: attributes --- { + "app.vitals.start.type": { + "type": "string", + "value": "warm" + }, + "app.vitals.start.value": { + "type": "double", + "value": 5000.0 + }, "app.vitals.start.warm.value": { "type": "double", "value": 5000.0 diff --git a/relay-event-normalization/src/event.rs b/relay-event-normalization/src/event.rs index 1fbf0057568..d8a71ec781b 100644 --- a/relay-event-normalization/src/event.rs +++ b/relay-event-normalization/src/event.rs @@ -1423,8 +1423,8 @@ fn normalize_mobile_measurements(measurements: &mut Measurements) { } const APP_START_SOURCES: [(&str, Option<&str>); 5] = [ - ("app_start_cold", Some("cold")), - ("app_start_warm", Some("warm")), + (APP_START_COLD, Some("cold")), + (APP_START_WARM, Some("warm")), (APP__VITALS__START__VALUE, None), (APP__VITALS__START__COLD__VALUE, None), (APP__VITALS__START__WARM__VALUE, None), diff --git a/relay-event-normalization/src/normalize/span/tag_extraction.rs b/relay-event-normalization/src/normalize/span/tag_extraction.rs index 08cad01fade..543ecf3db3f 100644 --- a/relay-event-normalization/src/normalize/span/tag_extraction.rs +++ b/relay-event-normalization/src/normalize/span/tag_extraction.rs @@ -9,6 +9,7 @@ use std::sync::LazyLock; use regex::Regex; use relay_base_schema::metrics::{DurationUnit, InformationUnit, MetricUnit}; +use relay_conventions::measurements::{APP_START_COLD, APP_START_WARM}; use relay_event_schema::protocol::{ AppContext, BrowserContext, DeviceContext, Event, GpuContext, Measurement, MonitorContext, OsContext, ProfileContext, ReplayContext, RuntimeContext, SentryTags, Span, Timestamp, @@ -1546,9 +1547,9 @@ pub fn span_op_to_category(op: &str) -> Option<&str> { /// Reads the event measurements to determine the start type of the event. fn get_event_start_type(event: &Event) -> Option<&'static str> { // Check the measurements on the event to determine what kind of start type the event is. - if event.measurement("app_start_cold").is_some() { + if event.measurement(APP_START_COLD).is_some() { Some("cold") - } else if event.measurement("app_start_warm").is_some() { + } else if event.measurement(APP_START_WARM).is_some() { Some("warm") } else { None diff --git a/relay-server/src/processing/spans/process.rs b/relay-server/src/processing/spans/process.rs index 4ffb3c0c391..9c2a5f9060b 100644 --- a/relay-server/src/processing/spans/process.rs +++ b/relay-server/src/processing/spans/process.rs @@ -1042,7 +1042,7 @@ mod tests { (DEVICE__MODEL, "iPhone17,5"), ], &[ - ("app_start_cold", 1234.0), + (APP__VITALS__START__COLD__VALUE, 1234.0), (APP__VITALS__TTFD__VALUE, 200_000.0), ], ); diff --git a/tests/integration/test_spans_standalone.py b/tests/integration/test_spans_standalone.py index 142943d47f5..d0b3db500d2 100644 --- a/tests/integration/test_spans_standalone.py +++ b/tests/integration/test_spans_standalone.py @@ -836,6 +836,7 @@ def test_mobile_measurements( "origin": "mobile", "exclusive_time": 104, "measurements": { + "app_start_cold": {"value": 0.123, "unit": "millisecond"}, "frames_slow": {"value": 1}, "frames_frozen": {"value": 2}, "frames_total": {"value": 4}, @@ -894,6 +895,16 @@ def test_mobile_measurements( "app.vitals.frames.total.count": {"value": 4.0, "type": "double"}, "frames_frozen_rate": {"value": 0.5, "type": "double"}, "frames_slow_rate": {"value": 0.25, "type": "double"}, + "app.vitals.start.cold.value": {"value": 0.123, "type": "double"}, + # These attributes are backfilled only in the V2 pipeline. In the legacy + # pipeline this logic doesn't exist. + **_if_dict( + mode == "v2", + { + "app.vitals.start.value": {"value": 0.123, "type": "double"}, + "app.vitals.start.type": {"value": "cold", "type": "string"}, + }, + ), **attributes, }, "downsampled_retention_days": 90, From cfc975acd06a9c12f9a1808da46df26a871c0d07 Mon Sep 17 00:00:00 2001 From: Sebastian Zivota Date: Tue, 9 Jun 2026 17:12:01 +0200 Subject: [PATCH 2/2] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b4e4ab4e7..624a49b978b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Correctly handle minidump objecstore upload failures. ([#6033](https://github.com/getsentry/relay/pull/6033)) - Add `client.address` attribute to known IP fields. ([#6058](https://github.com/getsentry/relay/pull/6058)) +- Fix a bug in mobile attribute normalization. ([#6065](https://github.com/getsentry/relay/pull/6065)) **Internal**: