relay_server/services/processor/span/
processing.rs

1//! Contains the processing-only functionality.
2
3use std::error::Error;
4use std::sync::Arc;
5
6use crate::envelope::{ContentType, Item, ItemType};
7use crate::managed::{ItemAction, ManagedEnvelope, TypedEnvelope};
8use crate::metrics_extraction::{event, generic};
9use crate::services::outcome::{DiscardReason, Outcome};
10use crate::services::processor::span::extract_transaction_span;
11use crate::services::processor::{
12    EventMetricsExtracted, ProcessingError, ProcessingExtractedMetrics, SpanGroup, SpansExtracted,
13    TransactionGroup, dynamic_sampling, event_type,
14};
15use crate::services::projects::project::ProjectInfo;
16use crate::utils::sample;
17use chrono::{DateTime, Utc};
18use relay_base_schema::events::EventType;
19use relay_base_schema::project::ProjectId;
20use relay_config::Config;
21use relay_dynamic_config::{
22    CombinedMetricExtractionConfig, ErrorBoundary, Feature, GlobalConfig, ProjectConfig,
23};
24use relay_event_normalization::span::ai::{extract_ai_data, map_ai_measurements_to_data};
25use relay_event_normalization::{
26    BorrowedSpanOpDefaults, ClientHints, CombinedMeasurementsConfig, FromUserAgentInfo,
27    GeoIpLookup, MeasurementsConfig, ModelCosts, PerformanceScoreConfig, RawUserAgentInfo,
28    SchemaProcessor, TimestampProcessor, TransactionNameRule, TransactionsProcessor,
29    TrimmingProcessor, normalize_measurements, normalize_performance_score,
30    normalize_transaction_name, span::tag_extraction, validate_span,
31};
32use relay_event_schema::processor::{ProcessingAction, ProcessingState, process_value};
33use relay_event_schema::protocol::{
34    BrowserContext, Event, EventId, IpAddr, Measurement, Measurements, Span, SpanData,
35};
36use relay_log::protocol::{Attachment, AttachmentType};
37use relay_metrics::{FractionUnit, MetricNamespace, MetricUnit, UnixTimestamp};
38use relay_pii::PiiProcessor;
39use relay_protocol::{Annotated, Empty, Value};
40use relay_quotas::DataCategory;
41use relay_sampling::evaluation::ReservoirEvaluator;
42use relay_spans::otel_trace::Span as OtelSpan;
43use thiserror::Error;
44
45#[derive(Error, Debug)]
46#[error(transparent)]
47struct ValidationError(#[from] anyhow::Error);
48
49#[allow(clippy::too_many_arguments)]
50pub async fn process(
51    managed_envelope: &mut TypedEnvelope<SpanGroup>,
52    event: &mut Annotated<Event>,
53    extracted_metrics: &mut ProcessingExtractedMetrics,
54    global_config: &GlobalConfig,
55    config: Arc<Config>,
56    project_id: ProjectId,
57    project_info: Arc<ProjectInfo>,
58    sampling_project_info: Option<Arc<ProjectInfo>>,
59    geo_lookup: Option<&GeoIpLookup>,
60    reservoir_counters: &ReservoirEvaluator<'_>,
61) {
62    use relay_event_normalization::RemoveOtherProcessor;
63
64    // We only implement trace-based sampling rules for now, which can be computed
65    // once for all spans in the envelope.
66    let sampling_result = dynamic_sampling::run(
67        managed_envelope,
68        event,
69        config.clone(),
70        project_info.clone(),
71        sampling_project_info,
72        reservoir_counters,
73    )
74    .await;
75
76    let span_metrics_extraction_config = match project_info.config.metric_extraction {
77        ErrorBoundary::Ok(ref config) if config.is_enabled() => Some(config),
78        _ => None,
79    };
80    let normalize_span_config = NormalizeSpanConfig::new(
81        &config,
82        global_config,
83        project_info.config(),
84        managed_envelope,
85        managed_envelope
86            .envelope()
87            .meta()
88            .client_addr()
89            .map(IpAddr::from),
90        geo_lookup,
91    );
92
93    let client_ip = managed_envelope.envelope().meta().client_addr();
94    let filter_settings = &project_info.config.filter_settings;
95    let sampling_decision = sampling_result.decision();
96
97    let mut span_count = 0;
98    managed_envelope.retain_items(|item| {
99        let mut annotated_span = match item.ty() {
100            ItemType::OtelSpan => match serde_json::from_slice::<OtelSpan>(&item.payload()) {
101                Ok(otel_span) => match relay_spans::otel_to_sentry_span(otel_span) {
102                    Ok(span) => Annotated::new(span),
103                    Err(err) => {
104                        relay_log::debug!("failed to convert OTel span to Sentry span: {:?}", err);
105                        return ItemAction::Drop(Outcome::Invalid(DiscardReason::InvalidJson));
106                    }
107                },
108                Err(err) => {
109                    relay_log::debug!("failed to parse OTel span: {}", err);
110                    return ItemAction::Drop(Outcome::Invalid(DiscardReason::InvalidJson));
111                }
112            },
113            ItemType::Span => match Annotated::<Span>::from_json_bytes(&item.payload()) {
114                Ok(span) => span,
115                Err(err) => {
116                    relay_log::debug!("failed to parse span: {}", err);
117                    return ItemAction::Drop(Outcome::Invalid(DiscardReason::InvalidJson));
118                }
119            },
120
121            _ => return ItemAction::Keep,
122        };
123
124        if let Err(e) = normalize(&mut annotated_span, normalize_span_config.clone()) {
125            relay_log::debug!("failed to normalize span: {}", e);
126            return ItemAction::Drop(Outcome::Invalid(match e {
127                ProcessingError::ProcessingFailed(ProcessingAction::InvalidTransaction(_))
128                | ProcessingError::InvalidTransaction
129                | ProcessingError::InvalidTimestamp => DiscardReason::InvalidSpan,
130                _ => DiscardReason::Internal,
131            }));
132        };
133
134        if let Some(span) = annotated_span.value() {
135            span_count += 1;
136
137            if let Err(filter_stat_key) = relay_filter::should_filter(
138                span,
139                client_ip,
140                filter_settings,
141                global_config.filters(),
142            ) {
143                relay_log::trace!(
144                    "filtering span {:?} that matched an inbound filter",
145                    span.span_id
146                );
147                return ItemAction::Drop(Outcome::Filtered(filter_stat_key));
148            }
149        }
150
151        if let Some(config) = span_metrics_extraction_config {
152            let Some(span) = annotated_span.value_mut() else {
153                return ItemAction::Drop(Outcome::Invalid(DiscardReason::Internal));
154            };
155            relay_log::trace!("extracting metrics from standalone span {:?}", span.span_id);
156
157            let ErrorBoundary::Ok(global_metrics_config) = &global_config.metric_extraction else {
158                return ItemAction::Drop(Outcome::Invalid(DiscardReason::Internal));
159            };
160
161            let metrics = generic::extract_metrics(
162                span,
163                CombinedMetricExtractionConfig::new(global_metrics_config, config),
164            );
165
166            extracted_metrics.extend_project_metrics(metrics, Some(sampling_decision));
167
168            if project_info.config.features.produces_spans() {
169                let transaction = span
170                    .data
171                    .value()
172                    .and_then(|d| d.segment_name.value())
173                    .cloned();
174                let bucket = event::create_span_root_counter(
175                    span,
176                    transaction,
177                    1,
178                    sampling_decision,
179                    project_id,
180                );
181                extracted_metrics.extend_sampling_metrics(bucket, Some(sampling_decision));
182            }
183
184            item.set_metrics_extracted(true);
185        }
186
187        if sampling_decision.is_drop() {
188            // Drop silently and not with an outcome, we only want to emit an outcome for the
189            // indexed category if the span was dropped by dynamic sampling.
190            // Dropping through the envelope will emit for both categories.
191            return ItemAction::DropSilently;
192        }
193
194        if let Err(e) = scrub(&mut annotated_span, &project_info.config) {
195            relay_log::error!("failed to scrub span: {e}");
196        }
197
198        // Remove additional fields.
199        process_value(
200            &mut annotated_span,
201            &mut RemoveOtherProcessor,
202            ProcessingState::root(),
203        )
204        .ok();
205
206        // Validate for kafka (TODO: this should be moved to kafka producer)
207        match validate(&mut annotated_span) {
208            Ok(res) => res,
209            Err(err) => {
210                relay_log::with_scope(
211                    |scope| {
212                        scope.add_attachment(Attachment {
213                            buffer: annotated_span.to_json().unwrap_or_default().into(),
214                            filename: "span.json".to_owned(),
215                            content_type: Some("application/json".to_owned()),
216                            ty: Some(AttachmentType::Attachment),
217                        })
218                    },
219                    || {
220                        relay_log::error!(
221                            error = &err as &dyn Error,
222                            source = "standalone",
223                            "invalid span"
224                        )
225                    },
226                );
227                return ItemAction::Drop(Outcome::Invalid(DiscardReason::InvalidSpan));
228            }
229        };
230
231        // Write back:
232        let mut new_item = Item::new(ItemType::Span);
233        let payload = match annotated_span.to_json() {
234            Ok(payload) => payload,
235            Err(err) => {
236                relay_log::debug!("failed to serialize span: {}", err);
237                return ItemAction::Drop(Outcome::Invalid(DiscardReason::Internal));
238            }
239        };
240        new_item.set_payload(ContentType::Json, payload);
241        new_item.set_metrics_extracted(item.metrics_extracted());
242
243        *item = new_item;
244
245        ItemAction::Keep
246    });
247
248    if sampling_decision.is_drop() {
249        relay_log::trace!(
250            span_count,
251            ?sampling_result,
252            "Dropped spans because of sampling rule",
253        );
254    }
255
256    if let Some(outcome) = sampling_result.into_dropped_outcome() {
257        managed_envelope.track_outcome(outcome, DataCategory::SpanIndexed, span_count);
258    }
259}
260
261fn add_sample_rate(measurements: &mut Annotated<Measurements>, name: &str, value: Option<f64>) {
262    let value = match value {
263        Some(value) if value > 0.0 => value,
264        _ => return,
265    };
266
267    let measurement = Annotated::new(Measurement {
268        value: Annotated::try_from(value),
269        unit: MetricUnit::Fraction(FractionUnit::Ratio).into(),
270    });
271
272    measurements
273        .get_or_insert_with(Measurements::default)
274        .insert(name.to_owned(), measurement);
275}
276
277#[allow(clippy::too_many_arguments)]
278pub fn extract_from_event(
279    managed_envelope: &mut TypedEnvelope<TransactionGroup>,
280    event: &Annotated<Event>,
281    global_config: &GlobalConfig,
282    config: Arc<Config>,
283    server_sample_rate: Option<f64>,
284    event_metrics_extracted: EventMetricsExtracted,
285    spans_extracted: SpansExtracted,
286) -> SpansExtracted {
287    // Only extract spans from transactions (not errors).
288    if event_type(event) != Some(EventType::Transaction) {
289        return spans_extracted;
290    };
291
292    if spans_extracted.0 {
293        return spans_extracted;
294    }
295
296    if let Some(sample_rate) = global_config.options.span_extraction_sample_rate {
297        if sample(sample_rate).is_discard() {
298            return spans_extracted;
299        }
300    }
301
302    let client_sample_rate = managed_envelope
303        .envelope()
304        .dsc()
305        .and_then(|ctx| ctx.sample_rate);
306
307    let mut add_span = |mut span: Span| {
308        add_sample_rate(
309            &mut span.measurements,
310            "client_sample_rate",
311            client_sample_rate,
312        );
313        add_sample_rate(
314            &mut span.measurements,
315            "server_sample_rate",
316            server_sample_rate,
317        );
318
319        let mut span = Annotated::new(span);
320
321        match validate(&mut span) {
322            Ok(span) => span,
323            Err(e) => {
324                relay_log::error!(
325                    error = &e as &dyn Error,
326                    span = ?span,
327                    source = "event",
328                    "invalid span"
329                );
330
331                managed_envelope.track_outcome(
332                    Outcome::Invalid(DiscardReason::InvalidSpan),
333                    relay_quotas::DataCategory::SpanIndexed,
334                    1,
335                );
336                return;
337            }
338        };
339
340        let span = match span.to_json() {
341            Ok(span) => span,
342            Err(e) => {
343                relay_log::error!(error = &e as &dyn Error, "Failed to serialize span");
344                managed_envelope.track_outcome(
345                    Outcome::Invalid(DiscardReason::InvalidSpan),
346                    relay_quotas::DataCategory::SpanIndexed,
347                    1,
348                );
349                return;
350            }
351        };
352
353        let mut item = Item::new(ItemType::Span);
354        item.set_payload(ContentType::Json, span);
355        // If metrics extraction happened for the event, it also happened for its spans:
356        item.set_metrics_extracted(event_metrics_extracted.0);
357
358        relay_log::trace!("Adding span to envelope");
359        managed_envelope.envelope_mut().add_item(item);
360    };
361
362    let Some(event) = event.value() else {
363        return spans_extracted;
364    };
365
366    let Some(transaction_span) = extract_transaction_span(
367        event,
368        config
369            .aggregator_config_for(MetricNamespace::Spans)
370            .max_tag_value_length,
371        &[],
372    ) else {
373        return spans_extracted;
374    };
375
376    // Add child spans as envelope items.
377    if let Some(child_spans) = event.spans.value() {
378        for span in child_spans {
379            let Some(inner_span) = span.value() else {
380                continue;
381            };
382            // HACK: clone the span to set the segment_id. This should happen
383            // as part of normalization once standalone spans reach wider adoption.
384            let mut new_span = inner_span.clone();
385            new_span.is_segment = Annotated::new(false);
386            new_span.is_remote = Annotated::new(false);
387            new_span.received = transaction_span.received.clone();
388            new_span.segment_id = transaction_span.segment_id.clone();
389            new_span.platform = transaction_span.platform.clone();
390
391            // If a profile is associated with the transaction, also associate it with its
392            // child spans.
393            new_span.profile_id = transaction_span.profile_id.clone();
394
395            add_span(new_span);
396        }
397    }
398
399    add_span(transaction_span);
400
401    SpansExtracted(true)
402}
403
404/// Removes the transaction in case the project has made the transition to spans-only.
405pub fn maybe_discard_transaction(
406    managed_envelope: &mut TypedEnvelope<TransactionGroup>,
407    event: Annotated<Event>,
408    project_info: Arc<ProjectInfo>,
409) -> Annotated<Event> {
410    if event_type(&event) == Some(EventType::Transaction)
411        && project_info.has_feature(Feature::DiscardTransaction)
412    {
413        managed_envelope.update();
414        return Annotated::empty();
415    }
416
417    event
418}
419/// Config needed to normalize a standalone span.
420#[derive(Clone, Debug)]
421struct NormalizeSpanConfig<'a> {
422    /// The time at which the event was received in this Relay.
423    received_at: DateTime<Utc>,
424    /// Allowed time range for spans.
425    timestamp_range: std::ops::Range<UnixTimestamp>,
426    /// The maximum allowed size of tag values in bytes. Longer values will be cropped.
427    max_tag_value_size: usize,
428    /// Configuration for generating performance score measurements for web vitals
429    performance_score: Option<&'a PerformanceScoreConfig>,
430    /// Configuration for measurement normalization in transaction events.
431    ///
432    /// Has an optional [`relay_event_normalization::MeasurementsConfig`] from both the project and the global level.
433    /// If at least one is provided, then normalization will truncate custom measurements
434    /// and add units of known built-in measurements.
435    measurements: Option<CombinedMeasurementsConfig<'a>>,
436    /// Configuration for AI model cost calculation
437    ai_model_costs: Option<&'a ModelCosts>,
438    /// The maximum length for names of custom measurements.
439    ///
440    /// Measurements with longer names are removed from the transaction event and replaced with a
441    /// metadata entry.
442    max_name_and_unit_len: usize,
443    /// Transaction name normalization rules.
444    tx_name_rules: &'a [TransactionNameRule],
445    /// The user agent parsed from the request.
446    user_agent: Option<String>,
447    /// Client hints parsed from the request.
448    client_hints: ClientHints<String>,
449    /// Hosts that are not replaced by "*" in HTTP span grouping.
450    allowed_hosts: &'a [String],
451    /// The IP address of the SDK that sent the event.
452    ///
453    /// When `{{auto}}` is specified and there is no other IP address in the payload, such as in the
454    /// `request` context, this IP address gets added to `span.data.client_address`.
455    client_ip: Option<IpAddr>,
456    /// An initialized GeoIP lookup.
457    geo_lookup: Option<&'a GeoIpLookup>,
458    span_op_defaults: BorrowedSpanOpDefaults<'a>,
459}
460
461impl<'a> NormalizeSpanConfig<'a> {
462    fn new(
463        config: &'a Config,
464        global_config: &'a GlobalConfig,
465        project_config: &'a ProjectConfig,
466        managed_envelope: &ManagedEnvelope,
467        client_ip: Option<IpAddr>,
468        geo_lookup: Option<&'a GeoIpLookup>,
469    ) -> Self {
470        let aggregator_config = config.aggregator_config_for(MetricNamespace::Spans);
471
472        Self {
473            received_at: managed_envelope.received_at(),
474            timestamp_range: aggregator_config.timestamp_range(),
475            max_tag_value_size: aggregator_config.max_tag_value_length,
476            performance_score: project_config.performance_score.as_ref(),
477            measurements: Some(CombinedMeasurementsConfig::new(
478                project_config.measurements.as_ref(),
479                global_config.measurements.as_ref(),
480            )),
481            ai_model_costs: match &global_config.ai_model_costs {
482                ErrorBoundary::Err(_) => None,
483                ErrorBoundary::Ok(costs) => Some(costs),
484            },
485            max_name_and_unit_len: aggregator_config
486                .max_name_length
487                .saturating_sub(MeasurementsConfig::MEASUREMENT_MRI_OVERHEAD),
488
489            tx_name_rules: &project_config.tx_name_rules,
490            user_agent: managed_envelope
491                .envelope()
492                .meta()
493                .user_agent()
494                .map(String::from),
495            client_hints: managed_envelope.meta().client_hints().clone(),
496            allowed_hosts: global_config.options.http_span_allowed_hosts.as_slice(),
497            client_ip,
498            geo_lookup,
499            span_op_defaults: global_config.span_op_defaults.borrow(),
500        }
501    }
502}
503
504fn set_segment_attributes(span: &mut Annotated<Span>) {
505    let Some(span) = span.value_mut() else { return };
506
507    // Identify INP spans or other WebVital spans and make sure they are not wrapped in a segment.
508    if let Some(span_op) = span.op.value() {
509        if span_op.starts_with("ui.interaction.") || span_op.starts_with("ui.webvital.") {
510            span.is_segment = None.into();
511            span.parent_span_id = None.into();
512            span.segment_id = None.into();
513            return;
514        }
515    }
516
517    let Some(span_id) = span.span_id.value() else {
518        return;
519    };
520
521    if let Some(segment_id) = span.segment_id.value() {
522        // The span is a segment if and only if the segment_id matches the span_id.
523        span.is_segment = (segment_id == span_id).into();
524    } else if span.parent_span_id.is_empty() {
525        // If the span has no parent, it is automatically a segment:
526        span.is_segment = true.into();
527    }
528
529    // If the span is a segment, always set the segment_id to the current span_id:
530    if span.is_segment.value() == Some(&true) {
531        span.segment_id = span.span_id.clone();
532    }
533}
534
535/// Normalizes a standalone span.
536fn normalize(
537    annotated_span: &mut Annotated<Span>,
538    config: NormalizeSpanConfig,
539) -> Result<(), ProcessingError> {
540    let NormalizeSpanConfig {
541        received_at,
542        timestamp_range,
543        max_tag_value_size,
544        performance_score,
545        measurements,
546        ai_model_costs,
547        max_name_and_unit_len,
548        tx_name_rules,
549        user_agent,
550        client_hints,
551        allowed_hosts,
552        client_ip,
553        geo_lookup,
554        span_op_defaults,
555    } = config;
556
557    set_segment_attributes(annotated_span);
558
559    // This follows the steps of `event::normalize`.
560
561    process_value(
562        annotated_span,
563        &mut SchemaProcessor,
564        ProcessingState::root(),
565    )?;
566
567    process_value(
568        annotated_span,
569        &mut TimestampProcessor,
570        ProcessingState::root(),
571    )?;
572
573    if let Some(span) = annotated_span.value() {
574        validate_span(span, Some(&timestamp_range))?;
575    }
576    process_value(
577        annotated_span,
578        &mut TransactionsProcessor::new(Default::default(), span_op_defaults),
579        ProcessingState::root(),
580    )?;
581
582    let Some(span) = annotated_span.value_mut() else {
583        return Err(ProcessingError::NoEventPayload);
584    };
585
586    // Replace missing / {{auto}} IPs:
587    // Transaction and error events require an explicit `{{auto}}` to derive the IP, but
588    // for spans we derive it by default:
589    if let Some(client_ip) = client_ip.as_ref() {
590        let ip = span.data.value().and_then(|d| d.client_address.value());
591        if ip.is_none_or(|ip| ip.is_auto()) {
592            span.data
593                .get_or_insert_with(Default::default)
594                .client_address = Annotated::new(client_ip.clone());
595        }
596    }
597
598    // Derive geo ip:
599    if let Some(geoip_lookup) = geo_lookup {
600        let data = span.data.get_or_insert_with(Default::default);
601        if let Some(ip) = data.client_address.value() {
602            if let Ok(Some(geo)) = geoip_lookup.lookup(ip.as_str()) {
603                data.user_geo_city = geo.city;
604                data.user_geo_country_code = geo.country_code;
605                data.user_geo_region = geo.region;
606                data.user_geo_subdivision = geo.subdivision;
607            }
608        }
609    }
610
611    populate_ua_fields(span, user_agent.as_deref(), client_hints.as_deref());
612
613    promote_span_data_fields(span);
614
615    if let Annotated(Some(ref mut measurement_values), ref mut meta) = span.measurements {
616        normalize_measurements(
617            measurement_values,
618            meta,
619            measurements,
620            Some(max_name_and_unit_len),
621            span.start_timestamp.0,
622            span.timestamp.0,
623        );
624    }
625
626    span.received = Annotated::new(received_at.into());
627
628    if let Some(transaction) = span
629        .data
630        .value_mut()
631        .as_mut()
632        .map(|data| &mut data.segment_name)
633    {
634        normalize_transaction_name(transaction, tx_name_rules);
635    }
636
637    // Tag extraction:
638    let is_mobile = false; // TODO: find a way to determine is_mobile from a standalone span.
639    let tags = tag_extraction::extract_tags(
640        span,
641        max_tag_value_size,
642        None,
643        None,
644        is_mobile,
645        None,
646        allowed_hosts,
647        geo_lookup,
648    );
649    span.sentry_tags = Annotated::new(tags);
650
651    normalize_performance_score(span, performance_score);
652
653    map_ai_measurements_to_data(span);
654    if let Some(model_costs_config) = ai_model_costs {
655        extract_ai_data(span, model_costs_config);
656    }
657
658    tag_extraction::extract_measurements(span, is_mobile);
659
660    process_value(
661        annotated_span,
662        &mut TrimmingProcessor::new(),
663        ProcessingState::root(),
664    )?;
665
666    Ok(())
667}
668
669fn populate_ua_fields(
670    span: &mut Span,
671    request_user_agent: Option<&str>,
672    mut client_hints: ClientHints<&str>,
673) {
674    let data = span.data.value_mut().get_or_insert_with(SpanData::default);
675
676    let user_agent = data.user_agent_original.value_mut();
677    if user_agent.is_none() {
678        *user_agent = request_user_agent.map(String::from);
679    } else {
680        // User agent in span payload should take precendence over request
681        // client hints.
682        client_hints = ClientHints::default();
683    }
684
685    if data.browser_name.value().is_none() {
686        if let Some(context) = BrowserContext::from_hints_or_ua(&RawUserAgentInfo {
687            user_agent: user_agent.as_deref(),
688            client_hints,
689        }) {
690            data.browser_name = context.name;
691        }
692    }
693}
694
695/// Promotes some fields from span.data as there are predefined places for certain fields.
696fn promote_span_data_fields(span: &mut Span) {
697    // INP spans sets some top level span attributes inside span.data so make sure to pull
698    // them out to the top level before further processing.
699    if let Some(data) = span.data.value_mut() {
700        if let Some(exclusive_time) = match data.exclusive_time.value() {
701            Some(Value::I64(exclusive_time)) => Some(*exclusive_time as f64),
702            Some(Value::U64(exclusive_time)) => Some(*exclusive_time as f64),
703            Some(Value::F64(exclusive_time)) => Some(*exclusive_time),
704            _ => None,
705        } {
706            span.exclusive_time = exclusive_time.into();
707            data.exclusive_time.set_value(None);
708        }
709
710        if let Some(profile_id) = match data.profile_id.value() {
711            Some(Value::String(profile_id)) => profile_id.parse().map(EventId).ok(),
712            _ => None,
713        } {
714            span.profile_id = profile_id.into();
715            data.profile_id.set_value(None);
716        }
717    }
718}
719
720fn scrub(
721    annotated_span: &mut Annotated<Span>,
722    project_config: &ProjectConfig,
723) -> Result<(), ProcessingError> {
724    if let Some(ref config) = project_config.pii_config {
725        let mut processor = PiiProcessor::new(config.compiled());
726        process_value(annotated_span, &mut processor, ProcessingState::root())?;
727    }
728    let pii_config = project_config
729        .datascrubbing_settings
730        .pii_config()
731        .map_err(|e| ProcessingError::PiiConfigError(e.clone()))?;
732    if let Some(config) = pii_config {
733        let mut processor = PiiProcessor::new(config.compiled());
734        process_value(annotated_span, &mut processor, ProcessingState::root())?;
735    }
736
737    Ok(())
738}
739
740/// We do not extract or ingest spans with missing fields if those fields are required on the Kafka topic.
741fn validate(span: &mut Annotated<Span>) -> Result<(), ValidationError> {
742    let inner = span
743        .value_mut()
744        .as_mut()
745        .ok_or(anyhow::anyhow!("empty span"))?;
746    let Span {
747        exclusive_time,
748        tags,
749        sentry_tags,
750        start_timestamp,
751        timestamp,
752        span_id,
753        trace_id,
754        ..
755    } = inner;
756
757    trace_id
758        .value()
759        .ok_or(anyhow::anyhow!("span is missing trace_id"))?;
760    span_id
761        .value()
762        .ok_or(anyhow::anyhow!("span is missing span_id"))?;
763
764    match (start_timestamp.value(), timestamp.value()) {
765        (Some(start), Some(end)) => {
766            if end < start {
767                return Err(ValidationError(anyhow::anyhow!(
768                    "end timestamp is smaller than start timestamp"
769                )));
770            }
771        }
772        (_, None) => {
773            return Err(ValidationError(anyhow::anyhow!(
774                "timestamp hard-required for spans"
775            )));
776        }
777        (None, _) => {
778            return Err(ValidationError(anyhow::anyhow!(
779                "start_timestamp hard-required for spans"
780            )));
781        }
782    }
783
784    exclusive_time
785        .value()
786        .ok_or(anyhow::anyhow!("missing exclusive_time"))?;
787
788    if let Some(sentry_tags) = sentry_tags.value_mut() {
789        if sentry_tags
790            .group
791            .value()
792            .is_some_and(|s| s.len() > 16 || s.chars().any(|c| !c.is_ascii_hexdigit()))
793        {
794            sentry_tags.group.set_value(None);
795        }
796
797        if sentry_tags
798            .status_code
799            .value()
800            .is_some_and(|s| s.parse::<u16>().is_err())
801        {
802            sentry_tags.group.set_value(None);
803        }
804    }
805    if let Some(tags) = tags.value_mut() {
806        tags.retain(|_, value| !value.value().is_empty())
807    }
808
809    Ok(())
810}
811
812#[cfg(test)]
813mod tests {
814    use std::collections::BTreeMap;
815    use std::sync::Arc;
816
817    use bytes::Bytes;
818    use once_cell::sync::Lazy;
819    use relay_event_schema::protocol::{Context, ContextInner, EventId, Timestamp, TraceContext};
820    use relay_event_schema::protocol::{Contexts, Event, Span};
821    use relay_protocol::get_value;
822    use relay_system::Addr;
823
824    use crate::envelope::Envelope;
825    use crate::managed::ManagedEnvelope;
826    use crate::services::processor::ProcessingGroup;
827    use crate::services::projects::project::ProjectInfo;
828
829    use super::*;
830
831    fn params() -> (
832        TypedEnvelope<TransactionGroup>,
833        Annotated<Event>,
834        Arc<ProjectInfo>,
835    ) {
836        let bytes = Bytes::from(
837            r#"{"event_id":"9ec79c33ec9942ab8353589fcb2e04dc","dsn":"https://e12d836b15bb49d7bbf99e64295d995b:@sentry.io/42","trace":{"trace_id":"89143b0763095bd9c9955e8175d1fb23","public_key":"e12d836b15bb49d7bbf99e64295d995b","sample_rate":"0.2"}}
838{"type":"transaction"}
839{}
840"#,
841        );
842
843        let dummy_envelope = Envelope::parse_bytes(bytes).unwrap();
844        let mut project_info = ProjectInfo::default();
845        project_info
846            .config
847            .features
848            .0
849            .insert(Feature::ExtractCommonSpanMetricsFromEvent);
850        let project_info = Arc::new(project_info);
851
852        let event = Event {
853            ty: EventType::Transaction.into(),
854            start_timestamp: Timestamp(DateTime::from_timestamp(0, 0).unwrap()).into(),
855            timestamp: Timestamp(DateTime::from_timestamp(1, 0).unwrap()).into(),
856
857            contexts: Contexts(BTreeMap::from([(
858                "trace".into(),
859                ContextInner(Context::Trace(Box::new(TraceContext {
860                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
861                    span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
862                    exclusive_time: 1000.0.into(),
863                    ..Default::default()
864                })))
865                .into(),
866            )]))
867            .into(),
868            ..Default::default()
869        };
870
871        let managed_envelope = ManagedEnvelope::new(dummy_envelope, Addr::dummy(), Addr::dummy());
872        let managed_envelope = (managed_envelope, ProcessingGroup::Transaction)
873            .try_into()
874            .unwrap();
875
876        let event = Annotated::from(event);
877
878        (managed_envelope, event, project_info)
879    }
880
881    #[test]
882    fn extract_sampled_default() {
883        let global_config = GlobalConfig::default();
884        let config = Arc::new(Config::default());
885        assert!(global_config.options.span_extraction_sample_rate.is_none());
886        let (mut managed_envelope, event, _) = params();
887        extract_from_event(
888            &mut managed_envelope,
889            &event,
890            &global_config,
891            config,
892            None,
893            EventMetricsExtracted(false),
894            SpansExtracted(false),
895        );
896        assert!(
897            managed_envelope
898                .envelope()
899                .items()
900                .any(|item| item.ty() == &ItemType::Span),
901            "{:?}",
902            managed_envelope.envelope()
903        );
904    }
905
906    #[test]
907    fn extract_sampled_explicit() {
908        let mut global_config = GlobalConfig::default();
909        global_config.options.span_extraction_sample_rate = Some(1.0);
910        let config = Arc::new(Config::default());
911        let (mut managed_envelope, event, _) = params();
912        extract_from_event(
913            &mut managed_envelope,
914            &event,
915            &global_config,
916            config,
917            None,
918            EventMetricsExtracted(false),
919            SpansExtracted(false),
920        );
921        assert!(
922            managed_envelope
923                .envelope()
924                .items()
925                .any(|item| item.ty() == &ItemType::Span),
926            "{:?}",
927            managed_envelope.envelope()
928        );
929    }
930
931    #[test]
932    fn extract_sampled_dropped() {
933        let mut global_config = GlobalConfig::default();
934        global_config.options.span_extraction_sample_rate = Some(0.0);
935        let config = Arc::new(Config::default());
936        let (mut managed_envelope, event, _) = params();
937        extract_from_event(
938            &mut managed_envelope,
939            &event,
940            &global_config,
941            config,
942            None,
943            EventMetricsExtracted(false),
944            SpansExtracted(false),
945        );
946        assert!(
947            !managed_envelope
948                .envelope()
949                .items()
950                .any(|item| item.ty() == &ItemType::Span),
951            "{:?}",
952            managed_envelope.envelope()
953        );
954    }
955
956    #[test]
957    fn extract_sample_rates() {
958        let mut global_config = GlobalConfig::default();
959        global_config.options.span_extraction_sample_rate = Some(1.0); // force enable
960        let config = Arc::new(Config::default());
961        let (mut managed_envelope, event, _) = params(); // client sample rate is 0.2
962        extract_from_event(
963            &mut managed_envelope,
964            &event,
965            &global_config,
966            config,
967            Some(0.1),
968            EventMetricsExtracted(false),
969            SpansExtracted(false),
970        );
971
972        let span = managed_envelope
973            .envelope()
974            .items()
975            .find(|item| item.ty() == &ItemType::Span)
976            .unwrap();
977
978        let span = Annotated::<Span>::from_json_bytes(&span.payload()).unwrap();
979        let measurements = span.value().and_then(|s| s.measurements.value());
980
981        insta::assert_debug_snapshot!(measurements, @r###"
982        Some(
983            Measurements(
984                {
985                    "client_sample_rate": Measurement {
986                        value: 0.2,
987                        unit: Fraction(
988                            Ratio,
989                        ),
990                    },
991                    "server_sample_rate": Measurement {
992                        value: 0.1,
993                        unit: Fraction(
994                            Ratio,
995                        ),
996                    },
997                },
998            ),
999        )
1000        "###);
1001    }
1002
1003    #[test]
1004    fn segment_no_overwrite() {
1005        let mut span: Annotated<Span> = Annotated::from_json(
1006            r#"{
1007            "is_segment": true,
1008            "span_id": "fa90fdead5f74052",
1009            "parent_span_id": "fa90fdead5f74051"
1010        }"#,
1011        )
1012        .unwrap();
1013        set_segment_attributes(&mut span);
1014        assert_eq!(get_value!(span.is_segment!), &true);
1015        assert_eq!(get_value!(span.segment_id!).to_string(), "fa90fdead5f74052");
1016    }
1017
1018    #[test]
1019    fn segment_overwrite_because_of_segment_id() {
1020        let mut span: Annotated<Span> = Annotated::from_json(
1021            r#"{
1022         "is_segment": false,
1023         "span_id": "fa90fdead5f74052",
1024         "segment_id": "fa90fdead5f74052",
1025         "parent_span_id": "fa90fdead5f74051"
1026     }"#,
1027        )
1028        .unwrap();
1029        set_segment_attributes(&mut span);
1030        assert_eq!(get_value!(span.is_segment!), &true);
1031    }
1032
1033    #[test]
1034    fn segment_overwrite_because_of_missing_parent() {
1035        let mut span: Annotated<Span> = Annotated::from_json(
1036            r#"{
1037         "is_segment": false,
1038         "span_id": "fa90fdead5f74052"
1039     }"#,
1040        )
1041        .unwrap();
1042        set_segment_attributes(&mut span);
1043        assert_eq!(get_value!(span.is_segment!), &true);
1044        assert_eq!(get_value!(span.segment_id!).to_string(), "fa90fdead5f74052");
1045    }
1046
1047    #[test]
1048    fn segment_no_parent_but_segment() {
1049        let mut span: Annotated<Span> = Annotated::from_json(
1050            r#"{
1051         "span_id": "fa90fdead5f74052",
1052         "segment_id": "ea90fdead5f74051"
1053     }"#,
1054        )
1055        .unwrap();
1056        set_segment_attributes(&mut span);
1057        assert_eq!(get_value!(span.is_segment!), &false);
1058        assert_eq!(get_value!(span.segment_id!).to_string(), "ea90fdead5f74051");
1059    }
1060
1061    #[test]
1062    fn segment_only_parent() {
1063        let mut span: Annotated<Span> = Annotated::from_json(
1064            r#"{
1065         "parent_span_id": "fa90fdead5f74051"
1066     }"#,
1067        )
1068        .unwrap();
1069        set_segment_attributes(&mut span);
1070        assert_eq!(get_value!(span.is_segment), None);
1071        assert_eq!(get_value!(span.segment_id), None);
1072    }
1073
1074    #[test]
1075    fn not_segment_but_inp_span() {
1076        let mut span: Annotated<Span> = Annotated::from_json(
1077            r#"{
1078         "op": "ui.interaction.click",
1079         "is_segment": false,
1080         "parent_span_id": "fa90fdead5f74051"
1081     }"#,
1082        )
1083        .unwrap();
1084        set_segment_attributes(&mut span);
1085        assert_eq!(get_value!(span.is_segment), None);
1086        assert_eq!(get_value!(span.segment_id), None);
1087    }
1088
1089    #[test]
1090    fn segment_but_inp_span() {
1091        let mut span: Annotated<Span> = Annotated::from_json(
1092            r#"{
1093         "op": "ui.interaction.click",
1094         "segment_id": "fa90fdead5f74051",
1095         "is_segment": true,
1096         "parent_span_id": "fa90fdead5f74051"
1097     }"#,
1098        )
1099        .unwrap();
1100        set_segment_attributes(&mut span);
1101        assert_eq!(get_value!(span.is_segment), None);
1102        assert_eq!(get_value!(span.segment_id), None);
1103    }
1104
1105    #[test]
1106    fn keep_browser_name() {
1107        let mut span: Annotated<Span> = Annotated::from_json(
1108            r#"{
1109                "data": {
1110                    "browser.name": "foo"
1111                }
1112            }"#,
1113        )
1114        .unwrap();
1115        populate_ua_fields(
1116            span.value_mut().as_mut().unwrap(),
1117            None,
1118            ClientHints::default(),
1119        );
1120        assert_eq!(get_value!(span.data.browser_name!), "foo");
1121    }
1122
1123    #[test]
1124    fn keep_browser_name_when_ua_present() {
1125        let mut span: Annotated<Span> = Annotated::from_json(
1126            r#"{
1127                "data": {
1128                    "browser.name": "foo",
1129                    "user_agent.original": "Mozilla/5.0 (-; -; -) - Chrome/18.0.1025.133 Mobile Safari/535.19"
1130                }
1131            }"#,
1132        )
1133            .unwrap();
1134        populate_ua_fields(
1135            span.value_mut().as_mut().unwrap(),
1136            None,
1137            ClientHints::default(),
1138        );
1139        assert_eq!(get_value!(span.data.browser_name!), "foo");
1140    }
1141
1142    #[test]
1143    fn derive_browser_name() {
1144        let mut span: Annotated<Span> = Annotated::from_json(
1145            r#"{
1146                "data": {
1147                    "user_agent.original": "Mozilla/5.0 (-; -; -) - Chrome/18.0.1025.133 Mobile Safari/535.19"
1148                }
1149            }"#,
1150        )
1151            .unwrap();
1152        populate_ua_fields(
1153            span.value_mut().as_mut().unwrap(),
1154            None,
1155            ClientHints::default(),
1156        );
1157        assert_eq!(
1158            get_value!(span.data.user_agent_original!),
1159            "Mozilla/5.0 (-; -; -) - Chrome/18.0.1025.133 Mobile Safari/535.19"
1160        );
1161        assert_eq!(get_value!(span.data.browser_name!), "Chrome Mobile");
1162    }
1163
1164    #[test]
1165    fn keep_user_agent_when_meta_is_present() {
1166        let mut span: Annotated<Span> = Annotated::from_json(
1167            r#"{
1168                "data": {
1169                    "user_agent.original": "Mozilla/5.0 (-; -; -) - Chrome/18.0.1025.133 Mobile Safari/535.19"
1170                }
1171            }"#,
1172        )
1173            .unwrap();
1174        populate_ua_fields(
1175            span.value_mut().as_mut().unwrap(),
1176            Some(
1177                "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; ONS Internet Explorer 6.1; .NET CLR 1.1.4322)",
1178            ),
1179            ClientHints::default(),
1180        );
1181        assert_eq!(
1182            get_value!(span.data.user_agent_original!),
1183            "Mozilla/5.0 (-; -; -) - Chrome/18.0.1025.133 Mobile Safari/535.19"
1184        );
1185        assert_eq!(get_value!(span.data.browser_name!), "Chrome Mobile");
1186    }
1187
1188    #[test]
1189    fn derive_user_agent() {
1190        let mut span: Annotated<Span> = Annotated::from_json(r#"{}"#).unwrap();
1191        populate_ua_fields(
1192            span.value_mut().as_mut().unwrap(),
1193            Some(
1194                "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; ONS Internet Explorer 6.1; .NET CLR 1.1.4322)",
1195            ),
1196            ClientHints::default(),
1197        );
1198        assert_eq!(
1199            get_value!(span.data.user_agent_original!),
1200            "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; ONS Internet Explorer 6.1; .NET CLR 1.1.4322)"
1201        );
1202        assert_eq!(get_value!(span.data.browser_name!), "IE");
1203    }
1204
1205    #[test]
1206    fn keep_user_agent_when_client_hints_are_present() {
1207        let mut span: Annotated<Span> = Annotated::from_json(
1208            r#"{
1209                "data": {
1210                    "user_agent.original": "Mozilla/5.0 (-; -; -) - Chrome/18.0.1025.133 Mobile Safari/535.19"
1211                }
1212            }"#,
1213        )
1214            .unwrap();
1215        populate_ua_fields(
1216            span.value_mut().as_mut().unwrap(),
1217            None,
1218            ClientHints {
1219                sec_ch_ua: Some(r#""Chromium";v="108", "Opera";v="94", "Not)A;Brand";v="99""#),
1220                ..Default::default()
1221            },
1222        );
1223        assert_eq!(
1224            get_value!(span.data.user_agent_original!),
1225            "Mozilla/5.0 (-; -; -) - Chrome/18.0.1025.133 Mobile Safari/535.19"
1226        );
1227        assert_eq!(get_value!(span.data.browser_name!), "Chrome Mobile");
1228    }
1229
1230    #[test]
1231    fn derive_client_hints() {
1232        let mut span: Annotated<Span> = Annotated::from_json(r#"{}"#).unwrap();
1233        populate_ua_fields(
1234            span.value_mut().as_mut().unwrap(),
1235            None,
1236            ClientHints {
1237                sec_ch_ua: Some(r#""Chromium";v="108", "Opera";v="94", "Not)A;Brand";v="99""#),
1238                ..Default::default()
1239            },
1240        );
1241        assert_eq!(get_value!(span.data.user_agent_original), None);
1242        assert_eq!(get_value!(span.data.browser_name!), "Opera");
1243    }
1244
1245    static GEO_LOOKUP: Lazy<GeoIpLookup> = Lazy::new(|| {
1246        GeoIpLookup::open("../relay-event-normalization/tests/fixtures/GeoIP2-Enterprise-Test.mmdb")
1247            .unwrap()
1248    });
1249
1250    fn normalize_config() -> NormalizeSpanConfig<'static> {
1251        NormalizeSpanConfig {
1252            received_at: DateTime::from_timestamp_nanos(0),
1253            timestamp_range: UnixTimestamp::from_datetime(
1254                DateTime::<Utc>::from_timestamp_millis(1000).unwrap(),
1255            )
1256            .unwrap()
1257                ..UnixTimestamp::from_datetime(DateTime::<Utc>::MAX_UTC).unwrap(),
1258            max_tag_value_size: 200,
1259            performance_score: None,
1260            measurements: None,
1261            ai_model_costs: None,
1262            max_name_and_unit_len: 200,
1263            tx_name_rules: &[],
1264            user_agent: None,
1265            client_hints: ClientHints::default(),
1266            allowed_hosts: &[],
1267            client_ip: Some(IpAddr("2.125.160.216".to_owned())),
1268            geo_lookup: Some(&GEO_LOOKUP),
1269            span_op_defaults: Default::default(),
1270        }
1271    }
1272
1273    #[test]
1274    fn user_ip_from_client_ip_without_auto() {
1275        let mut span = Annotated::from_json(
1276            r#"{
1277            "start_timestamp": 0,
1278            "timestamp": 1,
1279            "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
1280            "span_id": "922dda2462ea4ac2",
1281            "data": {
1282                "client.address": "2.125.160.216"
1283            }
1284        }"#,
1285        )
1286        .unwrap();
1287
1288        normalize(&mut span, normalize_config()).unwrap();
1289
1290        assert_eq!(
1291            get_value!(span.data.client_address!).as_str(),
1292            "2.125.160.216"
1293        );
1294        assert_eq!(get_value!(span.data.user_geo_city!), "Boxford");
1295    }
1296
1297    #[test]
1298    fn user_ip_from_client_ip_with_auto() {
1299        let mut span = Annotated::from_json(
1300            r#"{
1301            "start_timestamp": 0,
1302            "timestamp": 1,
1303            "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
1304            "span_id": "922dda2462ea4ac2",
1305            "data": {
1306                "client.address": "{{auto}}"
1307            }
1308        }"#,
1309        )
1310        .unwrap();
1311
1312        normalize(&mut span, normalize_config()).unwrap();
1313
1314        assert_eq!(
1315            get_value!(span.data.client_address!).as_str(),
1316            "2.125.160.216"
1317        );
1318        assert_eq!(get_value!(span.data.user_geo_city!), "Boxford");
1319    }
1320
1321    #[test]
1322    fn user_ip_from_client_ip_with_missing() {
1323        let mut span = Annotated::from_json(
1324            r#"{
1325            "start_timestamp": 0,
1326            "timestamp": 1,
1327            "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
1328            "span_id": "922dda2462ea4ac2"
1329        }"#,
1330        )
1331        .unwrap();
1332
1333        normalize(&mut span, normalize_config()).unwrap();
1334
1335        assert_eq!(
1336            get_value!(span.data.client_address!).as_str(),
1337            "2.125.160.216"
1338        );
1339        assert_eq!(get_value!(span.data.user_geo_city!), "Boxford");
1340    }
1341
1342    #[test]
1343    fn exclusive_time_inside_span_data_i64() {
1344        let mut span = Annotated::from_json(
1345            r#"{
1346            "start_timestamp": 0,
1347            "timestamp": 1,
1348            "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
1349            "span_id": "922dda2462ea4ac2",
1350            "data": {
1351                "sentry.exclusive_time": 123
1352            }
1353        }"#,
1354        )
1355        .unwrap();
1356
1357        normalize(&mut span, normalize_config()).unwrap();
1358
1359        let data = get_value!(span.data!);
1360        assert_eq!(data.exclusive_time, Annotated::empty());
1361        assert_eq!(*get_value!(span.exclusive_time!), 123.0);
1362    }
1363
1364    #[test]
1365    fn exclusive_time_inside_span_data_f64() {
1366        let mut span = Annotated::from_json(
1367            r#"{
1368            "start_timestamp": 0,
1369            "timestamp": 1,
1370            "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
1371            "span_id": "922dda2462ea4ac2",
1372            "data": {
1373                "sentry.exclusive_time": 123.0
1374            }
1375        }"#,
1376        )
1377        .unwrap();
1378
1379        normalize(&mut span, normalize_config()).unwrap();
1380
1381        let data = get_value!(span.data!);
1382        assert_eq!(data.exclusive_time, Annotated::empty());
1383        assert_eq!(*get_value!(span.exclusive_time!), 123.0);
1384    }
1385
1386    #[test]
1387    fn normalize_inp_spans() {
1388        let mut span = Annotated::from_json(
1389            r#"{
1390              "data": {
1391                "sentry.origin": "auto.http.browser.inp",
1392                "sentry.op": "ui.interaction.click",
1393                "release": "frontend@0735d75a05afe8d34bb0950f17c332eb32988862",
1394                "environment": "prod",
1395                "profile_id": "480ffcc911174ade9106b40ffbd822f5",
1396                "replay_id": "f39c5eb6539f4e49b9ad2b95226bc120",
1397                "transaction": "/replays",
1398                "user_agent.original": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
1399                "sentry.exclusive_time": 128.0
1400              },
1401              "description": "div.app-3diuwe.e88zkai6 > span.app-ksj0rb.e88zkai4",
1402              "op": "ui.interaction.click",
1403              "parent_span_id": "88457c3c28f4c0c6",
1404              "span_id": "be0e95480798a2a9",
1405              "start_timestamp": 1732635523.5048,
1406              "timestamp": 1732635523.6328,
1407              "trace_id": "bdaf4823d1c74068af238879e31e1be9",
1408              "origin": "auto.http.browser.inp",
1409              "exclusive_time": 128,
1410              "measurements": {
1411                "inp": {
1412                  "value": 128,
1413                  "unit": "millisecond"
1414                }
1415              },
1416              "segment_id": "88457c3c28f4c0c6"
1417        }"#,
1418        )
1419            .unwrap();
1420
1421        normalize(&mut span, normalize_config()).unwrap();
1422
1423        let data = get_value!(span.data!);
1424
1425        assert_eq!(data.exclusive_time, Annotated::empty());
1426        assert_eq!(*get_value!(span.exclusive_time!), 128.0);
1427
1428        assert_eq!(data.profile_id, Annotated::empty());
1429        assert_eq!(
1430            get_value!(span.profile_id!),
1431            &EventId("480ffcc911174ade9106b40ffbd822f5".parse().unwrap())
1432        );
1433    }
1434}