Skip to content

Commit 74fef3d

Browse files
authored
Merge branch 'master' into elramen-nit1
2 parents f7bddbd + 46a37c4 commit 74fef3d

2 files changed

Lines changed: 259 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
**Features**:
66

77
- Enable OTLP endpoints by default. ([#5951](https://github.com/getsentry/relay/pull/5951))
8+
- Backfill `app.vitals.start.screen` for V1 app-start transactions from the transaction name. ([#5960](https://github.com/getsentry/relay/pull/5960))
89
- Enable performance score calculation for V2 spans. ([#5947](https://github.com/getsentry/relay/pull/5947))
910

1011
**Bug Fixes**:

relay-event-normalization/src/event.rs

Lines changed: 258 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ use relay_base_schema::metrics::{
1414
DurationUnit, FractionUnit, MetricUnit, can_be_valid_metric_name,
1515
};
1616
use 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
};
1920
use relay_conventions::interpolate;
2021
use 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

14181424
fn 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+
14791533
fn 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

Comments
 (0)