Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(spans): Extract INP metrics from spans #2969

Merged
merged 21 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

- Emit a usage metric for total spans. ([#3007](https://github.com/getsentry/relay/pull/3007))
- Drop spans ending outside the valid timestamp range. ([#3013](https://github.com/getsentry/relay/pull/3013))
- Extract INP metrics from spans. ([#2969](https://github.com/getsentry/relay/pull/2969))

## 24.1.1

Expand All @@ -28,7 +29,8 @@
- Copy event measurements to span & normalize span measurements. ([#2953](https://github.com/getsentry/relay/pull/2953))
- Add `allow_negative` to `BuiltinMeasurementKey`. Filter out negative BuiltinMeasurements if `allow_negative` is false. ([#2982](https://github.com/getsentry/relay/pull/2982))
- Add possiblity to block metrics or their tags with glob-patterns. ([#2954](https://github.com/getsentry/relay/pull/2954), [#2973](https://github.com/getsentry/relay/pull/2973))
- Extract INP metrics from spans. ([#2969](https://github.com/getsentry/relay/pull/2969))
- Forward profiles of non-sampled transactions. ([#2940](https://github.com/getsentry/relay/pull/2940))
- Enable throttled periodic unspool of the buffered envelopes. ([#2993](https://github.com/getsentry/relay/pull/2993))

**Bug Fixes**:

Expand Down
2 changes: 1 addition & 1 deletion relay-dynamic-config/src/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ fn span_metrics() -> impl IntoIterator<Item = MetricSpec> {
let is_mobile_sdk = RuleCondition::eq("span.sentry_tags.mobile", "true");

let is_allowed_browser = RuleCondition::eq(
"span.browser.name",
"span.sentry_tags.browser.name",
vec!["Chrome", "Firefox", "Safari", "Edge", "Opera"],
);

Expand Down
192 changes: 77 additions & 115 deletions relay-event-normalization/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@ use relay_event_schema::protocol::{
IpAddr, Level, LogEntry, Measurement, Measurements, NelContext, Request, SpanAttribute,
SpanStatus, Tags, Timestamp, User,
};
use relay_protocol::{Annotated, Empty, Error, ErrorKind, Meta, Object, RuleCondition, Value};
use relay_protocol::{Annotated, Empty, Error, ErrorKind, Meta, Object, Value};
use smallvec::SmallVec;

use crate::normalize::request;
use crate::span::tag_extraction::{self, extract_span_tags};
use crate::utils::{self, MAX_DURATION_MOBILE_MS};
use crate::PerformanceScoreProfile;
use crate::{
breakdowns, legacy, mechanism, schema, span, stacktrace, transactions, trimming, user_agent,
BreakdownsConfig, DynamicMeasurementsConfig, GeoIpLookup, PerformanceScoreConfig,
Expand Down Expand Up @@ -218,7 +217,7 @@ fn normalize(event: &mut Event, meta: &mut Meta, config: &NormalizationConfig) {
config.measurements.clone(),
config.max_name_and_unit_len,
); // Measurements are part of the metric extraction
normalize_event_performance_score(event, config.performance_score);
normalize_performance_score(event, config.performance_score);
normalize_breakdowns(event, config.breakdowns_config); // Breakdowns are part of the metric extraction too

let _ = processor::apply(&mut event.request, |request, _| {
Expand Down Expand Up @@ -632,7 +631,7 @@ pub fn normalize_measurements(
///
/// This computes score from vital measurements, using config options to define how it is
/// calculated.
fn normalize_event_performance_score(
pub fn normalize_performance_score(
event: &mut Event,
performance_score: Option<&PerformanceScoreConfig>,
) {
Expand All @@ -644,117 +643,80 @@ fn normalize_event_performance_score(
if !condition.matches(event) {
continue;
}
let Some(measurements) = event.measurements.value_mut() else {
return;
};
normalize_performance_score_inner(measurements, profile);
}
}
}

/// Computes performance score measurements for a span.
///
/// This computes score from vital measurements, using config options to define how it is
/// calculated.
pub fn normalize_span_performance_score(
browser_name: String,
measurements: &mut Measurements,
performance_score: Option<&PerformanceScoreConfig>,
) {
let Some(performance_score) = performance_score else {
return;
};
for profile in &performance_score.profiles {
if let Some(condition) = &profile.condition {
// HACK: we support only checking browser name for spans
match condition {
RuleCondition::Eq(condition) => {
if condition.name != "event.contexts.browser.name" {
if let Some(measurements) = event.measurements.value_mut() {
let mut should_add_total = false;
if profile.score_components.iter().any(|c| {
!measurements.contains_key(c.measurement.as_str())
&& c.weight.abs() >= f64::EPSILON
&& !c.optional
}) {
// All non-optional measurements with a profile weight greater than 0 are
// required to exist on the event. Skip calculating performance scores if
// a measurement with weight is missing.
break;
}
let mut score_total = 0.0f64;
let mut weight_total = 0.0f64;
for component in &profile.score_components {
// Skip optional components if they are not present on the event.
if component.optional
&& !measurements.contains_key(component.measurement.as_str())
{
continue;
}
if condition.value != browser_name {
continue;
weight_total += component.weight;
}
if weight_total.abs() < f64::EPSILON {
// All components are optional or have a weight of `0`. We cannot compute
// component weights, so we bail.
break;
}
for component in &profile.score_components {
// Optional measurements that are not present are given a weight of 0.
let mut normalized_component_weight = 0.0;
if let Some(value) = measurements.get_value(component.measurement.as_str()) {
normalized_component_weight = component.weight / weight_total;
let cdf = utils::calculate_cdf_score(
value.max(0.0), // Webvitals can't be negative, but we need to clamp in case of bad data.
component.p10,
component.p50,
);
let component_score = cdf * normalized_component_weight;
score_total += component_score;
should_add_total = true;

measurements.insert(
format!("score.{}", component.measurement),
Measurement {
value: component_score.into(),
unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
}
.into(),
);
}
normalize_performance_score_inner(measurements, profile);
measurements.insert(
format!("score.weight.{}", component.measurement),
Measurement {
value: normalized_component_weight.into(),
unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
}
.into(),
);
}
// Condition not supported
_ => continue,
}
}
}
}

fn normalize_performance_score_inner(
measurements: &mut Measurements,
profile: &PerformanceScoreProfile,
) {
let mut should_add_total = false;
if profile.score_components.iter().any(|c| {
!measurements.contains_key(c.measurement.as_str())
&& c.weight.abs() >= f64::EPSILON
&& !c.optional
}) {
// All non-optional measurements with a profile weight greater than 0 are
// required to exist on the event. Skip calculating performance scores if
// a measurement with weight is missing.
return;
}
let mut score_total = 0.0f64;
let mut weight_total = 0.0f64;
for component in &profile.score_components {
// Skip optional components if they are not present on the event.
if component.optional && !measurements.contains_key(component.measurement.as_str()) {
continue;
}
weight_total += component.weight;
}
if weight_total.abs() < f64::EPSILON {
// All components are optional or have a weight of `0`. We cannot compute
// component weights, so we bail.
return;
}
for component in &profile.score_components {
// Optional measurements that are not present are given a weight of 0.
let mut normalized_component_weight = 0.0;
if let Some(value) = measurements.get_value(component.measurement.as_str()) {
normalized_component_weight = component.weight / weight_total;
let cdf = utils::calculate_cdf_score(
value.max(0.0), // Webvitals can't be negative, but we need to clamp in case of bad data.
component.p10,
component.p50,
);
let component_score = cdf * normalized_component_weight;
score_total += component_score;
should_add_total = true;

measurements.insert(
format!("score.{}", component.measurement),
Measurement {
value: component_score.into(),
unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
if should_add_total {
measurements.insert(
"score.total".to_owned(),
Measurement {
value: score_total.into(),
unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
}
.into(),
);
}
.into(),
);
}
measurements.insert(
format!("score.weight.{}", component.measurement),
Measurement {
value: normalized_component_weight.into(),
unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
}
.into(),
);
}

if should_add_total {
measurements.insert(
"score.total".to_owned(),
Measurement {
value: score_total.into(),
unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
break; // Measurements have successfully been added, skip any other profiles.
}
.into(),
);
}
}
}

Expand Down Expand Up @@ -1753,7 +1715,7 @@ mod tests {
}))
.unwrap();

normalize_event_performance_score(&mut event, Some(&performance_score));
normalize_performance_score(&mut event, Some(&performance_score));

insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
{
Expand Down Expand Up @@ -1901,7 +1863,7 @@ mod tests {
}))
.unwrap();

normalize_event_performance_score(&mut event, Some(&performance_score));
normalize_performance_score(&mut event, Some(&performance_score));

insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
{
Expand Down Expand Up @@ -2049,7 +2011,7 @@ mod tests {
}))
.unwrap();

normalize_event_performance_score(&mut event, Some(&performance_score));
normalize_performance_score(&mut event, Some(&performance_score));

insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
{
Expand Down Expand Up @@ -2174,7 +2136,7 @@ mod tests {
}))
.unwrap();

normalize_event_performance_score(&mut event, Some(&performance_score));
normalize_performance_score(&mut event, Some(&performance_score));

insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
{
Expand Down Expand Up @@ -2257,7 +2219,7 @@ mod tests {
}))
.unwrap();

normalize_event_performance_score(&mut event, Some(&performance_score));
normalize_performance_score(&mut event, Some(&performance_score));

insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
{
Expand Down Expand Up @@ -2345,7 +2307,7 @@ mod tests {
}))
.unwrap();

normalize_event_performance_score(&mut event, Some(&performance_score));
normalize_performance_score(&mut event, Some(&performance_score));

insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
{
Expand Down Expand Up @@ -2397,7 +2359,7 @@ mod tests {
}))
.unwrap();

normalize_event_performance_score(&mut event, Some(&performance_score));
normalize_performance_score(&mut event, Some(&performance_score));

insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
{
Expand Down
2 changes: 1 addition & 1 deletion relay-event-normalization/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub use validation::{
};
pub mod replay;
pub use event::{
normalize_event, normalize_measurements, normalize_span_performance_score, NormalizationConfig,
normalize_event, normalize_measurements, normalize_performance_score, NormalizationConfig,
};
pub use normalize::breakdowns::*;
pub use normalize::*;
Expand Down
8 changes: 8 additions & 0 deletions relay-event-schema/src/protocol/span.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ mod tests {
"data": {
"foo": {"bar": 1},
"foo.bar": 2
},
"measurements": {
"some": {"value": 100.0}
}
}"#,
)
Expand All @@ -255,6 +258,11 @@ mod tests {
assert_eq!(span.get_value("span.data"), None);
assert_eq!(span.get_value("span.data."), None);
assert_eq!(span.get_value("span.data.x"), None);

assert_eq!(
span.get_value("span.measurements.some.value"),
Some(Val::F64(100.0))
);
}

#[test]
Expand Down
36 changes: 36 additions & 0 deletions relay-server/src/metrics_extraction/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1426,4 +1426,40 @@ mod tests {
assert_eq!(metric.tag("ttfd"), Some("ttfd"));
}
}

#[test]
fn test_extract_span_metrics_performance_score() {
let json = r#"
{
"op": "ui.interaction.click",
"parent_span_id": "8f5a2b8768cafb4e",
"span_id": "bd429c44b67a3eb4",
"start_timestamp": 1597976300.0000000,
"timestamp": 1597976302.0000000,
"exclusive_time": 2000.0,
"trace_id": "ff62a8b040f340bda5d830223def1d81",
"sentry_tags": {
"browser.name": "Chrome",
"op": "ui.interaction.click"
},
"measurements": {
"score.total": {"value": 1.0},
"score.inp": {"value": 1.0},
"score.weight.inp": {"value": 1.0},
"inp": {"value": 1.0}
}
}
"#;
let span = Annotated::from_json(json).unwrap();
let metrics = extract_span_metrics(span.value().unwrap());

for mri in [
"d:spans/webvital.inp@millisecond",
"d:spans/webvital.score.inp@ratio",
"d:spans/webvital.score.total@ratio",
"d:spans/webvital.score.weight.inp@ratio",
] {
assert!(metrics.iter().any(|b| b.name == mri));
}
}
}
Loading
Loading