@@ -14,7 +14,8 @@ use relay_base_schema::metrics::{
1414 DurationUnit , FractionUnit , MetricUnit , can_be_valid_metric_name,
1515} ;
1616use relay_conventions:: consts:: {
17- APP__VITALS__START__TYPE , APP__VITALS__START__VALUE , SCORE__TOTAL ,
17+ APP__VITALS__START__COLD__VALUE , APP__VITALS__START__SCREEN , APP__VITALS__START__TYPE ,
18+ APP__VITALS__START__VALUE , APP__VITALS__START__WARM__VALUE , SCORE__TOTAL ,
1819} ;
1920use relay_conventions:: interpolate;
2021use relay_event_schema:: processor:: { self , ProcessingAction , ProcessingState , Processor } ;
@@ -1412,14 +1413,21 @@ fn normalize_mobile_measurements(measurements: &mut Measurements) {
14121413 filter_mobile_outliers ( measurements) ;
14131414}
14141415
1415- const APP_START_SOURCES : [ ( & str , & str ) ; 2 ] =
1416- [ ( "app_start_cold" , "cold" ) , ( "app_start_warm" , "warm" ) ] ;
1416+ const APP_START_SOURCES : [ ( & str , Option < & str > ) ; 5 ] = [
1417+ ( "app_start_cold" , Some ( "cold" ) ) ,
1418+ ( "app_start_warm" , Some ( "warm" ) ) ,
1419+ ( APP__VITALS__START__VALUE , None ) ,
1420+ ( APP__VITALS__START__COLD__VALUE , None ) ,
1421+ ( APP__VITALS__START__WARM__VALUE , None ) ,
1422+ ] ;
14171423
14181424fn backfill_app_vitals_start ( event : & mut Event ) {
14191425 if event. ty . value ( ) != Some ( & EventType :: Transaction ) {
14201426 return ;
14211427 }
14221428
1429+ backfill_app_vitals_start_screen ( event) ;
1430+
14231431 let already_set = event
14241432 . tags
14251433 . value ( )
@@ -1436,6 +1444,7 @@ fn backfill_app_vitals_start(event: &mut Event) {
14361444 APP_START_SOURCES
14371445 . iter ( )
14381446 . find_map ( |( measurement_name, start_type) | {
1447+ let start_type = ( * start_type) ?;
14391448 let measurement = event
14401449 . measurements
14411450 . value ( ) ?
@@ -1448,7 +1457,7 @@ fn backfill_app_vitals_start(event: &mut Event) {
14481457 }
14491458
14501459 let value = * measurement. value . value ( ) ?;
1451- Some ( ( * start_type, value) )
1460+ Some ( ( start_type, value) )
14521461 } )
14531462 else {
14541463 return ;
@@ -1476,6 +1485,51 @@ fn backfill_app_vitals_start(event: &mut Event) {
14761485 ) ;
14771486}
14781487
1488+ /// Backfills `app.vitals.start.screen` into root span data.
1489+ ///
1490+ /// This runs from the transaction-only app-start backfill and writes only when:
1491+ /// - the transaction name is a concrete screen name;
1492+ /// - the trace op is `"ui.load"`;
1493+ /// - the event contains an app-start measurement;
1494+ /// - the SDK did not already provide `app.vitals.start.screen`.
1495+ fn backfill_app_vitals_start_screen ( event : & mut Event ) {
1496+ let Some ( screen) = event. transaction . value ( ) else {
1497+ return ;
1498+ } ;
1499+ // TransactionsProcessor writes this placeholder for missing names before this backfill runs.
1500+ if screen. is_empty ( ) || screen == "<unlabeled transaction>" {
1501+ return ;
1502+ }
1503+
1504+ let has_app_start_measurement = event. measurements . value ( ) . is_some_and ( |measurements| {
1505+ APP_START_SOURCES
1506+ . iter ( )
1507+ . any ( |( measurement_name, _) | measurements. contains_key ( * measurement_name) )
1508+ } ) ;
1509+ if !has_app_start_measurement {
1510+ return ;
1511+ }
1512+
1513+ let screen = screen. to_owned ( ) ;
1514+ let Some ( trace_context) = event. context_mut :: < TraceContext > ( ) else {
1515+ return ;
1516+ } ;
1517+ if trace_context. op . as_str ( ) != Some ( "ui.load" )
1518+ || trace_context
1519+ . data
1520+ . value ( )
1521+ . is_some_and ( |data| data. other . contains_key ( APP__VITALS__START__SCREEN ) )
1522+ {
1523+ return ;
1524+ }
1525+
1526+ let data = trace_context. data . get_or_insert_with ( Default :: default) ;
1527+ data. other . insert (
1528+ APP__VITALS__START__SCREEN . to_owned ( ) ,
1529+ Annotated :: new ( Value :: String ( screen) ) ,
1530+ ) ;
1531+ }
1532+
14791533fn normalize_units ( measurements : & mut Measurements ) {
14801534 for ( name, measurement) in measurements. iter_mut ( ) {
14811535 let measurement = match measurement. value_mut ( ) {
@@ -1711,6 +1765,43 @@ mod tests {
17111765 . unwrap ( )
17121766 }
17131767
1768+ fn trace_context_data ( event : & Event ) -> & Annotated < SpanData > {
1769+ & event. context :: < TraceContext > ( ) . unwrap ( ) . data
1770+ }
1771+
1772+ fn app_vitals_start_screen_event (
1773+ ty : & str ,
1774+ transaction : Option < & str > ,
1775+ trace_op : & str ,
1776+ measurement : Option < & str > ,
1777+ existing_screen : Option < & str > ,
1778+ ) -> Event {
1779+ let mut payload = json ! ( {
1780+ "type" : ty,
1781+ "contexts" : { "trace" : { "op" : trace_op} } ,
1782+ "measurements" : { } ,
1783+ } ) ;
1784+
1785+ if let Some ( transaction) = transaction {
1786+ payload[ "transaction" ] = json ! ( transaction) ;
1787+ }
1788+
1789+ if let Some ( measurement) = measurement {
1790+ payload[ "measurements" ] = json ! ( {
1791+ measurement: { "value" : 1234.0 , "unit" : "millisecond" }
1792+ } ) ;
1793+ }
1794+
1795+ if let Some ( screen) = existing_screen {
1796+ payload[ "contexts" ] [ "trace" ] [ "data" ] = json ! ( { APP__VITALS__START__SCREEN : screen} ) ;
1797+ }
1798+
1799+ Annotated :: < Event > :: from_json ( & payload. to_string ( ) )
1800+ . unwrap ( )
1801+ . into_value ( )
1802+ . unwrap ( )
1803+ }
1804+
17141805 #[ test]
17151806 fn test_normalize_dist_none ( ) {
17161807 let mut dist = Annotated :: default ( ) ;
@@ -3306,6 +3397,169 @@ mod tests {
33063397 assert_debug_snapshot ! ( event. tags, @"~" ) ;
33073398 }
33083399
3400+ #[ test]
3401+ fn test_backfill_app_vitals_start_screen_from_legacy_measurement ( ) {
3402+ let json = r#"{
3403+ "type": "transaction",
3404+ "transaction": "MainActivity",
3405+ "contexts": {
3406+ "trace": {
3407+ "op": "ui.load"
3408+ }
3409+ },
3410+ "measurements": {
3411+ "app_start_cold": {
3412+ "value": 1234.0,
3413+ "unit": "millisecond"
3414+ }
3415+ }
3416+ }"# ;
3417+ let mut event = Annotated :: < Event > :: from_json ( json)
3418+ . unwrap ( )
3419+ . into_value ( )
3420+ . unwrap ( ) ;
3421+
3422+ backfill_app_vitals_start ( & mut event) ;
3423+
3424+ assert_annotated_snapshot ! ( trace_context_data( & event) , @r#"
3425+ {
3426+ "app.vitals.start.screen": "MainActivity"
3427+ }
3428+ "# ) ;
3429+ }
3430+
3431+ #[ test]
3432+ fn test_backfill_app_vitals_start_screen_from_dotted_measurement ( ) {
3433+ let mut event = app_vitals_start_screen_event (
3434+ "transaction" ,
3435+ Some ( "SettingsActivity" ) ,
3436+ "ui.load" ,
3437+ Some ( APP__VITALS__START__WARM__VALUE ) ,
3438+ None ,
3439+ ) ;
3440+
3441+ backfill_app_vitals_start ( & mut event) ;
3442+
3443+ assert_annotated_snapshot ! ( trace_context_data( & event) , @r#"
3444+ {
3445+ "app.vitals.start.screen": "SettingsActivity"
3446+ }
3447+ "# ) ;
3448+ }
3449+
3450+ #[ test]
3451+ fn test_backfill_app_vitals_start_screen_from_start_value_measurement ( ) {
3452+ let mut event = app_vitals_start_screen_event (
3453+ "transaction" ,
3454+ Some ( "ProfileActivity" ) ,
3455+ "ui.load" ,
3456+ Some ( APP__VITALS__START__VALUE ) ,
3457+ None ,
3458+ ) ;
3459+
3460+ backfill_app_vitals_start ( & mut event) ;
3461+
3462+ assert_annotated_snapshot ! ( trace_context_data( & event) , @r#"
3463+ {
3464+ "app.vitals.start.screen": "ProfileActivity"
3465+ }
3466+ "# ) ;
3467+ }
3468+
3469+ #[ test]
3470+ fn test_backfill_app_vitals_start_screen_requires_ui_load ( ) {
3471+ let mut event = app_vitals_start_screen_event (
3472+ "transaction" ,
3473+ Some ( "MainActivity" ) ,
3474+ "navigation" ,
3475+ Some ( "app_start_cold" ) ,
3476+ None ,
3477+ ) ;
3478+
3479+ backfill_app_vitals_start ( & mut event) ;
3480+
3481+ assert_annotated_snapshot ! ( trace_context_data( & event) , @"{}" ) ;
3482+ }
3483+
3484+ #[ test]
3485+ fn test_backfill_app_vitals_start_screen_requires_app_start_measurement ( ) {
3486+ let mut event = app_vitals_start_screen_event (
3487+ "transaction" ,
3488+ Some ( "MainActivity" ) ,
3489+ "ui.load" ,
3490+ None ,
3491+ None ,
3492+ ) ;
3493+
3494+ backfill_app_vitals_start ( & mut event) ;
3495+
3496+ assert_annotated_snapshot ! ( trace_context_data( & event) , @"{}" ) ;
3497+ }
3498+
3499+ #[ test]
3500+ fn test_backfill_app_vitals_start_screen_only_requires_measurement_key ( ) {
3501+ let json = r#"{
3502+ "type": "transaction",
3503+ "transaction": "MainActivity",
3504+ "contexts": {
3505+ "trace": {
3506+ "op": "ui.load"
3507+ }
3508+ },
3509+ "measurements": {
3510+ "app_start_cold": {
3511+ "unit": "millisecond"
3512+ }
3513+ }
3514+ }"# ;
3515+ let mut event = Annotated :: < Event > :: from_json ( json)
3516+ . unwrap ( )
3517+ . into_value ( )
3518+ . unwrap ( ) ;
3519+
3520+ backfill_app_vitals_start ( & mut event) ;
3521+
3522+ assert_annotated_snapshot ! ( trace_context_data( & event) , @r#"
3523+ {
3524+ "app.vitals.start.screen": "MainActivity"
3525+ }
3526+ "# ) ;
3527+ }
3528+
3529+ #[ test]
3530+ fn test_backfill_app_vitals_start_screen_preserves_existing_value ( ) {
3531+ let mut event = app_vitals_start_screen_event (
3532+ "transaction" ,
3533+ Some ( "MainActivity" ) ,
3534+ "ui.load" ,
3535+ Some ( "app_start_cold" ) ,
3536+ Some ( "SDKScreen" ) ,
3537+ ) ;
3538+
3539+ backfill_app_vitals_start ( & mut event) ;
3540+
3541+ assert_annotated_snapshot ! ( trace_context_data( & event) , @r#"
3542+ {
3543+ "app.vitals.start.screen": "SDKScreen"
3544+ }
3545+ "# ) ;
3546+ }
3547+
3548+ #[ test]
3549+ fn test_backfill_app_vitals_start_screen_requires_transaction_name ( ) {
3550+ let mut event = app_vitals_start_screen_event (
3551+ "transaction" ,
3552+ Some ( "<unlabeled transaction>" ) ,
3553+ "ui.load" ,
3554+ Some ( "app_start_cold" ) ,
3555+ None ,
3556+ ) ;
3557+
3558+ backfill_app_vitals_start ( & mut event) ;
3559+
3560+ assert_annotated_snapshot ! ( trace_context_data( & event) , @"{}" ) ;
3561+ }
3562+
33093563 #[ test]
33103564 fn test_computed_performance_score_transaction ( ) {
33113565 let json = r#"
0 commit comments