1use relay_conventions::attributes::{
2 BROWSER__WEB_VITAL__CLS__VALUE, BROWSER__WEB_VITAL__FCP__VALUE, BROWSER__WEB_VITAL__INP__VALUE,
3 BROWSER__WEB_VITAL__LCP__ELEMENT, BROWSER__WEB_VITAL__LCP__ID,
4 BROWSER__WEB_VITAL__LCP__LOAD_TIME, BROWSER__WEB_VITAL__LCP__RENDER_TIME,
5 BROWSER__WEB_VITAL__LCP__SIZE, BROWSER__WEB_VITAL__LCP__URL, BROWSER__WEB_VITAL__LCP__VALUE,
6 BROWSER__WEB_VITAL__TTFB__REQUEST_TIME, BROWSER__WEB_VITAL__TTFB__VALUE, SENTRY__ENVIRONMENT,
7 SENTRY__METRIC__SOURCE, SENTRY__ORIGIN, SENTRY__PAGELOAD__SPAN_ID, SENTRY__PLATFORM,
8 SENTRY__RELEASE, SENTRY__SDK__NAME, SENTRY__SDK__VERSION, SENTRY__SEGMENT__NAME,
9 USER_AGENT__ORIGINAL,
10};
11use relay_conventions::interpolate::browser__web_vital__cls__source__key;
12use relay_event_schema::protocol::{Attributes, SpanV2, TraceMetric};
13use relay_metrics::MetricUnit;
14use relay_protocol::Value;
15use std::collections::BTreeMap;
16
17const MAX_CLS_SOURCES: u32 = 128;
18
19const WEB_VITAL_SPAN_NAMES: [&str; 7] = [
20 "pageload",
21 "ui.webvital.lcp",
22 "ui.webvital.cls",
23 "ui.interaction.click",
24 "ui.interaction.hover",
25 "ui.interaction.drag",
26 "ui.interaction.press",
27];
28
29const COMMON_ATTRIBUTES: [&str; 9] = [
30 SENTRY__PAGELOAD__SPAN_ID,
31 SENTRY__ORIGIN,
32 SENTRY__SEGMENT__NAME,
33 USER_AGENT__ORIGINAL,
34 SENTRY__RELEASE,
35 SENTRY__ENVIRONMENT,
36 SENTRY__SDK__NAME,
37 SENTRY__SDK__VERSION,
38 SENTRY__PLATFORM,
39];
40
41const WEB_VITAL_LOOKUPS: [WebVital; 5] = [
42 WebVital {
43 attribute_value: BROWSER__WEB_VITAL__LCP__VALUE,
44 name: "browser.web_vital.lcp",
45 unit: MetricUnit::Duration(relay_metrics::DurationUnit::MilliSecond),
46 attribute_keys: &[
47 BROWSER__WEB_VITAL__LCP__ELEMENT,
48 BROWSER__WEB_VITAL__LCP__ID,
49 BROWSER__WEB_VITAL__LCP__URL,
50 BROWSER__WEB_VITAL__LCP__SIZE,
51 BROWSER__WEB_VITAL__LCP__LOAD_TIME,
52 BROWSER__WEB_VITAL__LCP__RENDER_TIME,
53 "score.lcp",
54 "score.weight.lcp",
55 "score.ratio.lcp",
56 ],
57 },
58 WebVital {
59 attribute_value: BROWSER__WEB_VITAL__CLS__VALUE,
60 name: "browser.web_vital.cls",
61 unit: MetricUnit::None,
62 attribute_keys: &["score.cls", "score.weight.cls", "score.ratio.cls"],
63 },
64 WebVital {
65 attribute_value: BROWSER__WEB_VITAL__INP__VALUE,
66 name: "browser.web_vital.inp",
67 unit: MetricUnit::Duration(relay_metrics::DurationUnit::MilliSecond),
68 attribute_keys: &[
69 "browser.web_vital.inp.target",
70 "browser.web_vital.inp.type",
71 "score.inp",
72 "score.weight.inp",
73 "score.ratio.inp",
74 ],
75 },
76 WebVital {
77 attribute_value: BROWSER__WEB_VITAL__FCP__VALUE,
78 name: "browser.web_vital.fcp",
79 unit: MetricUnit::Duration(relay_metrics::DurationUnit::MilliSecond),
80 attribute_keys: &["score.fcp", "score.weight.fcp", "score.ratio.fcp"],
81 },
82 WebVital {
83 attribute_value: BROWSER__WEB_VITAL__TTFB__VALUE,
84 name: "browser.web_vital.ttfb",
85 unit: MetricUnit::Duration(relay_metrics::DurationUnit::MilliSecond),
86 attribute_keys: &[
87 BROWSER__WEB_VITAL__TTFB__REQUEST_TIME,
88 "score.ttfb",
89 "score.ratio.ttfb",
90 "score.weight.ttfb",
91 ],
92 },
93];
94
95struct WebVital {
96 name: &'static str,
97 attribute_value: &'static str,
98 unit: MetricUnit,
99 attribute_keys: &'static [&'static str],
100}
101
102pub fn extract_web_vital_metrics(span: &SpanV2) -> Option<Vec<TraceMetric>> {
105 let attrs = span.attributes.0.as_ref()?;
106
107 let op_name = attrs.get_value("sentry.op")?.as_str()?;
108
109 if !WEB_VITAL_SPAN_NAMES.contains(&op_name) {
110 return None;
111 }
112
113 let mut results = vec![];
114
115 for web_vital in &WEB_VITAL_LOOKUPS {
116 let Some(value) = attrs.get_value(web_vital.attribute_value) else {
117 continue;
118 };
119
120 let Some(value) = value.as_f64() else {
121 continue;
122 };
123
124 let mut attributes = Attributes::new();
125
126 if web_vital.attribute_value == BROWSER__WEB_VITAL__CLS__VALUE {
130 for i in 1..MAX_CLS_SOURCES {
131 let attr_key = browser__web_vital__cls__source__key(i.to_string());
132 match attrs.get_attribute(&attr_key) {
133 Some(v) => attributes.insert(attr_key, v.value.clone()),
134 None => break,
135 }
136 }
137 }
138
139 for attr_key in web_vital.attribute_keys {
140 if let Some(v) = attrs.get_attribute(*attr_key) {
141 attributes.insert(*attr_key, v.value.clone());
142 }
143 }
144
145 for attr_key in COMMON_ATTRIBUTES {
146 if let Some(v) = attrs.get_attribute(attr_key) {
147 attributes.insert(attr_key, v.value.clone());
148 }
149 }
150
151 attributes.insert(SENTRY__METRIC__SOURCE, "span");
153
154 let trace_metric = TraceMetric {
155 timestamp: span.start_timestamp.clone(),
156 trace_id: span.trace_id.clone(),
157 span_id: span.span_id.clone(),
158 name: web_vital.name.to_owned().into(),
159 ty: relay_event_schema::protocol::MetricType::Distribution.into(),
160 unit: web_vital.unit.into(),
161 value: Value::F64(value).into(),
162 attributes: attributes.into(),
163 other: BTreeMap::default(),
164 };
165
166 results.push(trace_metric);
167 }
168
169 if results.is_empty() {
170 None
171 } else {
172 Some(results)
173 }
174}