Skip to main content

relay_event_normalization/
validation.rs

1use std::ops::Range;
2
3use chrono::{DateTime, Duration, Utc};
4use relay_base_schema::events::EventType;
5use relay_common::time::UnixTimestamp;
6use relay_event_schema::processor::{
7    self, ProcessingAction, ProcessingResult, ProcessingState, Processor,
8};
9use relay_event_schema::protocol::{Event, Span, Timestamp, TraceContext};
10use relay_protocol::{Annotated, ErrorKind, Meta};
11
12use crate::{ClockDriftProcessor, TimestampProcessor};
13
14/// Configuration for [`validate_event`].
15#[derive(Debug, Default)]
16pub struct EventValidationConfig {
17    /// The time at which the event was received in this Relay.
18    ///
19    /// This timestamp is persisted into the event payload.
20    pub received_at: Option<DateTime<Utc>>,
21
22    /// The maximum amount of seconds an event can be dated in the past.
23    ///
24    /// If the event's timestamp is older, the received timestamp is assumed.
25    pub max_secs_in_past: Option<i64>,
26
27    /// The maximum amount of seconds an event can be predated into the future.
28    ///
29    /// If the event's timestamp lies further into the future, the received timestamp is assumed.
30    pub max_secs_in_future: Option<i64>,
31
32    /// Timestamp range in which a transaction must start and end.
33    ///
34    /// Transactions outside this range are invalid. The check is skipped if no range is provided.
35    pub transaction_timestamp_range: Option<Range<UnixTimestamp>>,
36
37    /// Controls whether the event has been validated before, in which case disables validation.
38    ///
39    /// By default, `is_validated` is disabled and event validation is run.
40    ///
41    /// Similar to `is_renormalize` for normalization, `sentry_relay` may configure this value.
42    pub is_validated: bool,
43}
44
45/// Validates an event.
46///
47/// Validation consists of performing multiple checks on the payload, based on
48/// the given configuration.
49///
50/// The returned [`ProcessingResult`] indicates whether the passed event is
51/// invalid and thus should be dropped.
52pub fn validate_event(
53    event: &mut Annotated<Event>,
54    config: &EventValidationConfig,
55) -> ProcessingResult {
56    if config.is_validated {
57        return Ok(());
58    }
59
60    let Annotated(Some(event), meta) = event else {
61        return Ok(());
62    };
63
64    // TimestampProcessor is required in validation, and requires normalizing
65    // timestamps before (especially clock drift changes).
66    normalize_timestamps(
67        event,
68        meta,
69        config.received_at,
70        config.max_secs_in_past,
71        config.max_secs_in_future,
72    );
73    TimestampProcessor.process_event(event, meta, ProcessingState::root())?;
74
75    if event.ty.value() == Some(&EventType::Transaction) {
76        validate_transaction_timestamps(event, config)?;
77        validate_trace_context(event)?;
78        end_all_spans(event);
79        validate_spans(event, config.transaction_timestamp_range.as_ref())?;
80    }
81
82    Ok(())
83}
84
85/// Validates the timestamp range and sets a default value.
86fn normalize_timestamps(
87    event: &mut Event,
88    meta: &mut Meta,
89    received_at: Option<DateTime<Utc>>,
90    max_secs_in_past: Option<i64>,
91    max_secs_in_future: Option<i64>,
92) {
93    let received_at = received_at.unwrap_or_else(Utc::now);
94
95    let mut sent_at = None;
96    let mut error_kind = ErrorKind::ClockDrift;
97
98    let _ = processor::apply(&mut event.timestamp, |timestamp, _meta| {
99        if let Some(secs) = max_secs_in_future
100            && *timestamp > received_at + Duration::seconds(secs)
101        {
102            error_kind = ErrorKind::FutureTimestamp;
103            sent_at = Some(*timestamp);
104            return Ok(());
105        }
106
107        if let Some(secs) = max_secs_in_past
108            && *timestamp < received_at - Duration::seconds(secs)
109        {
110            error_kind = ErrorKind::PastTimestamp;
111            sent_at = Some(*timestamp);
112            return Ok(());
113        }
114
115        Ok(())
116    });
117
118    let _ = ClockDriftProcessor::new(sent_at.map(|ts| ts.into_inner()), received_at)
119        .error_kind(error_kind)
120        .process_event(event, meta, ProcessingState::root());
121
122    // Apply this after clock drift correction, otherwise we will malform it.
123    event.received = Annotated::new(received_at.into());
124
125    if event.timestamp.value().is_none() {
126        event.timestamp.set_value(Some(received_at.into()));
127    }
128
129    let _ = processor::apply(&mut event.time_spent, |time_spent, _| {
130        validate_bounded_integer_field(*time_spent)
131    });
132}
133
134/// Returns whether the transaction's start and end timestamps are both set, start <= end and they
135/// are in `config.transaction_timestamp_range`.
136fn validate_transaction_timestamps(
137    transaction_event: &Event,
138    config: &EventValidationConfig,
139) -> ProcessingResult {
140    match (
141        transaction_event.start_timestamp.value(),
142        transaction_event.timestamp.value(),
143    ) {
144        (Some(start), Some(end)) => {
145            validate_timestamps(start, end, config.transaction_timestamp_range.as_ref())?;
146            Ok(())
147        }
148        (_, None) => Err(ProcessingAction::InvalidTransaction(
149            "timestamp hard-required for transaction events",
150        )),
151        // XXX: Maybe copy timestamp over?
152        (None, _) => Err(ProcessingAction::InvalidTransaction(
153            "start_timestamp hard-required for transaction events",
154        )),
155    }
156}
157
158/// Validates that start <= end timestamp and that both are inside the valid range.
159fn validate_timestamps(
160    start: &Timestamp,
161    end: &Timestamp,
162    valid_range: Option<&Range<UnixTimestamp>>,
163) -> ProcessingResult {
164    if end < start {
165        return Err(ProcessingAction::InvalidTransaction(
166            "end timestamp is smaller than start timestamp",
167        ));
168    }
169
170    let Some(range) = valid_range else {
171        return Ok(());
172    };
173
174    let Some(end) = UnixTimestamp::from_datetime(end.into_inner()) else {
175        return Err(ProcessingAction::InvalidTransaction(
176            "invalid unix timestamp",
177        ));
178    };
179    let Some(start) = UnixTimestamp::from_datetime(start.into_inner()) else {
180        return Err(ProcessingAction::InvalidTransaction(
181            "invalid unix timestamp",
182        ));
183    };
184
185    if start < range.start {
186        return Err(ProcessingAction::InvalidTransaction(
187            "start timestamp is out of the valid range for metrics",
188        ));
189    }
190    if range.end <= end {
191        return Err(ProcessingAction::InvalidTransaction(
192            "end timestamp is out of the valid range for metrics",
193        ));
194    }
195
196    Ok(())
197}
198
199/// Validates the trace context in a transaction.
200///
201/// A [`ProcessingResult`] error is returned if the context is not valid. The
202/// context is valid if the trace context meets the following conditions:
203/// - It exists.
204/// - It has a trace id.
205/// - It has a span id.
206fn validate_trace_context(transaction: &Event) -> ProcessingResult {
207    let Some(trace_context) = transaction.context::<TraceContext>() else {
208        return Err(ProcessingAction::InvalidTransaction(
209            "missing valid trace context",
210        ));
211    };
212
213    if trace_context.trace_id.value().is_none() {
214        return Err(ProcessingAction::InvalidTransaction(
215            "trace context is missing trace_id",
216        ));
217    }
218
219    if trace_context.span_id.value().is_none() {
220        return Err(ProcessingAction::InvalidTransaction(
221            "trace context is missing span_id",
222        ));
223    }
224
225    Ok(())
226}
227
228/// Copies the event's end timestamp into the spans that don't have one.
229fn end_all_spans(event: &mut Event) {
230    let spans = event.spans.value_mut().get_or_insert_with(Vec::new);
231    for span in spans {
232        if let Some(span) = span.value_mut()
233            && span.timestamp.value().is_none()
234        {
235            // event timestamp guaranteed to be `Some` due to validate_transaction call
236            span.timestamp.set_value(event.timestamp.value().cloned());
237            span.status = Annotated::new(relay_base_schema::spans::SpanStatus::DeadlineExceeded);
238        }
239    }
240}
241
242/// Validates the spans in the transaction.
243///
244/// A [`ProcessingResult`] error is returned if there's an invalid span. For
245/// span validity, see [`validate_transaction_span`].
246fn validate_spans(
247    transaction: &mut Event,
248    timestamp_range: Option<&Range<UnixTimestamp>>,
249) -> ProcessingResult {
250    let (Some(transaction_start), Some(transaction_end)) = (
251        transaction.start_timestamp.value().copied(),
252        transaction.timestamp.value().copied(),
253    ) else {
254        // Should never run into this since it is already validated earlier.
255        return Err(ProcessingAction::InvalidTransaction(
256            "timestamps hard-required for transaction events",
257        ));
258    };
259
260    let Some(spans) = transaction.spans.value_mut() else {
261        return Ok(());
262    };
263
264    for span in spans {
265        if let Some(span) = span.value_mut() {
266            validate_transaction_span(span, transaction_start, transaction_end, timestamp_range)?;
267        } else {
268            return Err(ProcessingAction::InvalidTransaction(
269                "spans must be valid in transaction event",
270            ));
271        }
272    }
273
274    Ok(())
275}
276
277/// Ensures that `timestamp` is within `range`, otherwise replacing it with `fallback`.
278///
279/// Returns an error if `timestamp` has no value or cannot be parsed to [`UnixTimestamp`].
280fn enforce_timestamp_in_range(
281    timestamp: &mut Annotated<Timestamp>,
282    fallback: Timestamp,
283    range: Option<&Range<UnixTimestamp>>,
284) -> ProcessingResult {
285    let Annotated(Some(ts), meta) = timestamp else {
286        return Err(ProcessingAction::InvalidTransaction(
287            "span is missing timestamp",
288        ));
289    };
290
291    let Some(uts) = UnixTimestamp::from_datetime(ts.into_inner()) else {
292        return Err(ProcessingAction::InvalidTransaction(
293            "invalid unix timestamp",
294        ));
295    };
296
297    if let Some(range) = range
298        && (uts < range.start || range.end <= uts)
299    {
300        meta.set_original_value(Some(ts.to_string()));
301        meta.add_error(ErrorKind::InvalidData);
302        *ts = fallback;
303    }
304
305    Ok(())
306}
307
308/// Validates a transaction span.
309///
310/// Similar to [`validate_standalone_span`] but will replace timestamps outside `range` with the
311/// timestamps of the transaction.
312fn validate_transaction_span(
313    span: &mut Span,
314    transaction_start: Timestamp,
315    transaction_end: Timestamp,
316    range: Option<&Range<UnixTimestamp>>,
317) -> ProcessingResult {
318    enforce_timestamp_in_range(&mut span.start_timestamp, transaction_start, range)?;
319    enforce_timestamp_in_range(&mut span.timestamp, transaction_end, range)?;
320
321    if let (Some(start), Some(end)) = (span.start_timestamp.value(), span.timestamp.value())
322        && end < start
323    {
324        return Err(ProcessingAction::InvalidTransaction(
325            "end timestamp is smaller than start timestamp",
326        ));
327    }
328
329    validate_span_ids(span)
330}
331
332/// Validates a standalone span.
333///
334/// A [`ProcessingResult`] error is returned when the span is invalid. A span is
335/// valid if all the following conditions are met:
336/// - A start timestamp exists.
337/// - An end timestamp exists.
338/// - Start timestamp must be no later than end timestamp.
339/// - Both start and end timestamps must be within `timestamp_range`.
340/// - A trace id exists.
341/// - A span id exists.
342pub fn validate_standalone_span(
343    span: &Span,
344    timestamp_range: Option<&Range<UnixTimestamp>>,
345) -> ProcessingResult {
346    match (span.start_timestamp.value(), span.timestamp.value()) {
347        (Some(start), Some(end)) => {
348            validate_timestamps(start, end, timestamp_range)?;
349        }
350        (_, None) => {
351            // XXX: Maybe do the same as event.timestamp?
352            return Err(ProcessingAction::InvalidTransaction(
353                "span is missing timestamp",
354            ));
355        }
356        (None, _) => {
357            // XXX: Maybe copy timestamp over?
358            return Err(ProcessingAction::InvalidTransaction(
359                "span is missing start_timestamp",
360            ));
361        }
362    }
363
364    validate_span_ids(span)
365}
366
367/// Validates that the span has a `trace_id` and `span_id`.
368fn validate_span_ids(span: &Span) -> ProcessingResult {
369    if span.trace_id.value().is_none() {
370        return Err(ProcessingAction::InvalidTransaction(
371            "span is missing trace_id",
372        ));
373    }
374
375    if span.span_id.value().is_none() {
376        return Err(ProcessingAction::InvalidTransaction(
377            "span is missing span_id",
378        ));
379    }
380
381    Ok(())
382}
383
384/// Validate fields that go into a `sentry.models.BoundedIntegerField`.
385fn validate_bounded_integer_field(value: u64) -> ProcessingResult {
386    if value < 2_147_483_647 {
387        Ok(())
388    } else {
389        Err(ProcessingAction::DeleteValueHard)
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use chrono::TimeZone;
396    use relay_base_schema::spans::SpanStatus;
397    use relay_event_schema::protocol::Contexts;
398    use relay_protocol::{Object, get_value};
399
400    use super::*;
401
402    fn new_test_event() -> Annotated<Event> {
403        let start = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
404        let end = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap();
405        Annotated::new(Event {
406            ty: Annotated::new(EventType::Transaction),
407            transaction: Annotated::new("/".to_owned()),
408            start_timestamp: Annotated::new(start.into()),
409            timestamp: Annotated::new(end.into()),
410            contexts: {
411                let mut contexts = Contexts::new();
412                contexts.add(TraceContext {
413                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
414                    span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
415                    op: Annotated::new("http.server".to_owned()),
416                    ..Default::default()
417                });
418                Annotated::new(contexts)
419            },
420            spans: Annotated::new(vec![Annotated::new(Span {
421                start_timestamp: Annotated::new(start.into()),
422                timestamp: Annotated::new(end.into()),
423                trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
424                span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
425                op: Annotated::new("db.statement".to_owned()),
426                ..Default::default()
427            })]),
428            ..Default::default()
429        })
430    }
431
432    #[test]
433    fn test_timestamp_added_if_missing() {
434        let mut event = Annotated::new(Event::default());
435        assert!(get_value!(event.timestamp).is_none());
436        assert!(validate_event(&mut event, &EventValidationConfig::default()).is_ok());
437        assert!(get_value!(event.timestamp).is_some());
438    }
439
440    #[test]
441    fn test_discards_when_timestamp_out_of_range() {
442        let mut event = new_test_event();
443
444        assert!(matches!(
445            validate_event(
446                &mut event,
447                &EventValidationConfig {
448                    transaction_timestamp_range: Some(UnixTimestamp::now()..UnixTimestamp::now()),
449                    is_validated: false,
450                    ..Default::default()
451                }
452            ),
453            Err(ProcessingAction::InvalidTransaction(
454                "start timestamp is out of the valid range for metrics"
455            ))
456        ));
457    }
458
459    #[test]
460    fn test_discards_when_missing_start_timestamp() {
461        let mut event = Annotated::new(Event {
462            ty: Annotated::new(EventType::Transaction),
463            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
464            ..Default::default()
465        });
466
467        assert_eq!(
468            validate_event(&mut event, &EventValidationConfig::default()),
469            Err(ProcessingAction::InvalidTransaction(
470                "start_timestamp hard-required for transaction events"
471            ))
472        );
473    }
474
475    #[test]
476    fn test_discards_on_missing_contexts_map() {
477        let mut event = Annotated::new(Event {
478            ty: Annotated::new(EventType::Transaction),
479            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
480            start_timestamp: Annotated::new(
481                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
482            ),
483            ..Default::default()
484        });
485
486        assert_eq!(
487            validate_event(
488                &mut event,
489                &EventValidationConfig {
490                    transaction_timestamp_range: None,
491                    is_validated: false,
492                    ..Default::default()
493                }
494            ),
495            Err(ProcessingAction::InvalidTransaction(
496                "missing valid trace context"
497            ))
498        );
499    }
500
501    #[test]
502    fn test_discards_on_missing_context() {
503        let mut event = Annotated::new(Event {
504            ty: Annotated::new(EventType::Transaction),
505            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
506            start_timestamp: Annotated::new(
507                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
508            ),
509            contexts: Annotated::new(Contexts::new()),
510            ..Default::default()
511        });
512
513        assert_eq!(
514            validate_event(
515                &mut event,
516                &EventValidationConfig {
517                    transaction_timestamp_range: None,
518                    is_validated: false,
519                    ..Default::default()
520                }
521            ),
522            Err(ProcessingAction::InvalidTransaction(
523                "missing valid trace context"
524            ))
525        );
526    }
527
528    #[test]
529    fn test_discards_on_null_context() {
530        let mut event = Annotated::new(Event {
531            ty: Annotated::new(EventType::Transaction),
532            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
533            start_timestamp: Annotated::new(
534                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
535            ),
536            contexts: Annotated::new(Contexts({
537                let mut contexts = Object::new();
538                contexts.insert("trace".to_owned(), Annotated::empty());
539                contexts
540            })),
541            ..Default::default()
542        });
543
544        assert_eq!(
545            validate_event(&mut event, &EventValidationConfig::default()),
546            Err(ProcessingAction::InvalidTransaction(
547                "missing valid trace context"
548            ))
549        );
550    }
551
552    #[test]
553    fn test_discards_on_missing_trace_id_in_context() {
554        let mut event = Annotated::new(Event {
555            ty: Annotated::new(EventType::Transaction),
556            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
557            start_timestamp: Annotated::new(
558                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
559            ),
560            contexts: {
561                let mut contexts = Contexts::new();
562                contexts.add(TraceContext::default());
563                Annotated::new(contexts)
564            },
565            ..Default::default()
566        });
567
568        assert_eq!(
569            validate_event(&mut event, &EventValidationConfig::default()),
570            Err(ProcessingAction::InvalidTransaction(
571                "trace context is missing trace_id"
572            ))
573        );
574    }
575
576    #[test]
577    fn test_discards_on_missing_span_id_in_context() {
578        let mut event = Annotated::new(Event {
579            ty: Annotated::new(EventType::Transaction),
580            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
581            start_timestamp: Annotated::new(
582                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
583            ),
584            contexts: {
585                let mut contexts = Contexts::new();
586                contexts.add(TraceContext {
587                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
588                    ..Default::default()
589                });
590                Annotated::new(contexts)
591            },
592            ..Default::default()
593        });
594
595        assert_eq!(
596            validate_event(&mut event, &EventValidationConfig::default()),
597            Err(ProcessingAction::InvalidTransaction(
598                "trace context is missing span_id"
599            ))
600        );
601    }
602
603    #[test]
604    fn test_discards_transaction_event_with_nulled_out_span() {
605        let mut event = Annotated::new(Event {
606            ty: Annotated::new(EventType::Transaction),
607            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
608            start_timestamp: Annotated::new(
609                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
610            ),
611            contexts: {
612                let mut contexts = Contexts::new();
613                contexts.add(TraceContext {
614                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
615                    span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
616                    op: Annotated::new("http.server".to_owned()),
617                    ..Default::default()
618                });
619                Annotated::new(contexts)
620            },
621            spans: Annotated::new(vec![Annotated::empty()]),
622            ..Default::default()
623        });
624
625        assert_eq!(
626            validate_event(&mut event, &EventValidationConfig::default()),
627            Err(ProcessingAction::InvalidTransaction(
628                "spans must be valid in transaction event"
629            ))
630        );
631    }
632
633    #[test]
634    fn test_discards_transaction_event_with_span_with_missing_start_timestamp() {
635        let mut event = Annotated::new(Event {
636            ty: Annotated::new(EventType::Transaction),
637            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
638            start_timestamp: Annotated::new(
639                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
640            ),
641            contexts: {
642                let mut contexts = Contexts::new();
643                contexts.add(TraceContext {
644                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
645                    span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
646                    op: Annotated::new("http.server".to_owned()),
647                    ..Default::default()
648                });
649                Annotated::new(contexts)
650            },
651            spans: Annotated::new(vec![Annotated::new(Span {
652                timestamp: Annotated::new(
653                    Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
654                ),
655                ..Default::default()
656            })]),
657            ..Default::default()
658        });
659
660        assert_eq!(
661            validate_event(&mut event, &EventValidationConfig::default()),
662            Err(ProcessingAction::InvalidTransaction(
663                "span is missing timestamp"
664            ))
665        );
666    }
667
668    #[test]
669    fn test_discards_transaction_event_with_span_with_missing_trace_id() {
670        let mut event = Annotated::new(Event {
671            ty: Annotated::new(EventType::Transaction),
672            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
673            start_timestamp: Annotated::new(
674                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
675            ),
676            contexts: {
677                let mut contexts = Contexts::new();
678                contexts.add(TraceContext {
679                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
680                    span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
681                    op: Annotated::new("http.server".to_owned()),
682                    ..Default::default()
683                });
684                Annotated::new(contexts)
685            },
686            spans: Annotated::new(vec![Annotated::new(Span {
687                timestamp: Annotated::new(
688                    Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
689                ),
690                start_timestamp: Annotated::new(
691                    Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
692                ),
693                ..Default::default()
694            })]),
695            ..Default::default()
696        });
697
698        assert_eq!(
699            validate_event(&mut event, &EventValidationConfig::default()),
700            Err(ProcessingAction::InvalidTransaction(
701                "span is missing trace_id"
702            ))
703        );
704    }
705
706    #[test]
707    fn test_discards_transaction_event_with_span_with_missing_span_id() {
708        let mut event = Annotated::new(Event {
709            ty: Annotated::new(EventType::Transaction),
710            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
711            start_timestamp: Annotated::new(
712                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
713            ),
714            contexts: {
715                let mut contexts = Contexts::new();
716                contexts.add(TraceContext {
717                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
718                    span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
719                    op: Annotated::new("http.server".to_owned()),
720                    ..Default::default()
721                });
722                Annotated::new(contexts)
723            },
724            spans: Annotated::new(vec![Annotated::new(Span {
725                timestamp: Annotated::new(
726                    Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
727                ),
728                start_timestamp: Annotated::new(
729                    Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
730                ),
731                trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
732                ..Default::default()
733            })]),
734            ..Default::default()
735        });
736
737        assert_eq!(
738            validate_event(&mut event, &EventValidationConfig::default()),
739            Err(ProcessingAction::InvalidTransaction(
740                "span is missing span_id"
741            ))
742        );
743    }
744
745    #[test]
746    fn test_reject_stale_transaction() {
747        let json = r#"{
748  "event_id": "52df9022835246eeb317dbd739ccd059",
749  "start_timestamp": -2,
750  "timestamp": -1
751}"#;
752        let mut transaction = Annotated::<Event>::from_json(json).unwrap();
753        let res = validate_event(&mut transaction, &EventValidationConfig::default());
754        assert_eq!(
755            res.unwrap_err().to_string(),
756            "invalid transaction event: timestamp is too stale"
757        );
758    }
759
760    /// Test that timestamp normalization updates a transaction's timestamps to
761    /// be acceptable, when both timestamps are similarly stale.
762    #[test]
763    fn test_accept_recent_transactions_with_stale_timestamps() {
764        let config = EventValidationConfig {
765            received_at: Some(Utc::now()),
766            max_secs_in_past: Some(2),
767            max_secs_in_future: Some(1),
768            is_validated: false,
769            ..Default::default()
770        };
771
772        let json = r#"{
773  "event_id": "52df9022835246eeb317dbd739ccd059",
774  "transaction": "I have a stale timestamp, but I'm recent!",
775  "start_timestamp": -2,
776  "timestamp": -1
777}"#;
778        let mut event = Annotated::<Event>::from_json(json).unwrap();
779
780        assert!(validate_event(&mut event, &config).is_ok());
781    }
782
783    /// Test that transactions are rejected as invalid when timestamp normalization isn't enough.
784    ///
785    /// When the end timestamp is recent but the start timestamp is stale, timestamp normalization
786    /// will fix the timestamps based on the end timestamp. The start timestamp will be more recent,
787    /// but not recent enough for the transaction to be accepted. The transaction will be rejected.
788    #[test]
789    fn test_reject_stale_transactions_after_timestamp_normalization() {
790        let now = Utc::now();
791        let config = EventValidationConfig {
792            received_at: Some(now),
793            max_secs_in_past: Some(2),
794            max_secs_in_future: Some(1),
795            is_validated: false,
796            ..Default::default()
797        };
798
799        let json = format!(
800            r#"{{
801          "event_id": "52df9022835246eeb317dbd739ccd059",
802          "transaction": "clockdrift is not enough to accept me :(",
803          "start_timestamp": -62135811111,
804          "timestamp": {}
805        }}"#,
806            now.timestamp()
807        );
808        let mut event = Annotated::<Event>::from_json(json.as_str()).unwrap();
809
810        assert_eq!(
811            validate_event(&mut event, &config).unwrap_err().to_string(),
812            "invalid transaction event: timestamp is too stale"
813        );
814    }
815
816    /// Validates an unfinished span in a transaction, and the transaction is accepted.
817    #[test]
818    fn test_accept_transactions_with_unfinished_spans() {
819        let json = r#"{
820  "event_id": "52df9022835246eeb317dbd739ccd059",
821  "type": "transaction",
822  "transaction": "I have a stale timestamp, but I'm recent!",
823  "start_timestamp": 1,
824  "timestamp": 2,
825  "contexts": {
826    "trace": {
827      "trace_id": "ff62a8b040f340bda5d830223def1d81",
828      "span_id": "bd429c44b67a3eb4"
829    }
830  },
831  "spans": [
832    {
833      "span_id": "bd429c44b67a3eb4",
834      "start_timestamp": 1,
835      "timestamp": null,
836      "trace_id": "ff62a8b040f340bda5d830223def1d81"
837    }
838  ]
839}"#;
840        let mut event = Annotated::<Event>::from_json(json).unwrap();
841
842        assert!(validate_event(&mut event, &EventValidationConfig::default()).is_ok());
843
844        let event = get_value!(event!);
845        let spans = &event.spans;
846        let span = get_value!(spans[0]!);
847
848        assert_eq!(span.timestamp, event.timestamp);
849        assert_eq!(span.status.value().unwrap(), &SpanStatus::DeadlineExceeded);
850    }
851
852    /// Validates an unfinished span is finished with the normalized transaction's timestamp.
853    #[test]
854    fn test_finish_spans_with_normalized_transaction_end_timestamp() {
855        let now = Utc::now();
856        let start_timestamp = (now - Duration::seconds(1)).timestamp();
857        let timestamp = now.timestamp();
858
859        let json = format!(
860            r#"{{
861  "event_id": "52df9022835246eeb317dbd739ccd059",
862  "type": "transaction",
863  "transaction": "I have a stale timestamp, but I'm recent!",
864  "start_timestamp": {start_timestamp},
865  "timestamp": {timestamp},
866  "contexts": {{
867    "trace": {{
868      "trace_id": "ff62a8b040f340bda5d830223def1d81",
869      "span_id": "bd429c44b67a3eb4"
870    }}
871    }},
872  "spans": [
873    {{
874      "span_id": "bd429c44b67a3eb4",
875      "start_timestamp": {start_timestamp},
876      "timestamp": null,
877      "trace_id": "ff62a8b040f340bda5d830223def1d81"
878    }}
879  ]
880    }}"#
881        );
882        let mut event = Annotated::<Event>::from_json(&json).unwrap();
883
884        validate_event(
885            &mut event,
886            &EventValidationConfig {
887                received_at: Some(now),
888                max_secs_in_past: Some(2),
889                max_secs_in_future: Some(1),
890                is_validated: false,
891                ..Default::default()
892            },
893        )
894        .unwrap();
895        validate_event(&mut event, &EventValidationConfig::default()).unwrap();
896
897        let event = get_value!(event!);
898        let spans = &event.spans;
899        let span = get_value!(spans[0]!);
900
901        assert_eq!(span.timestamp.value(), event.timestamp.value());
902    }
903
904    #[test]
905    fn test_skip_transaction_validation_on_renormalization() {
906        let json = r#"{
907  "event_id": "52df9022835246eeb317dbd739ccd059",
908  "type": "transaction",
909  "transaction": "I'm invalid because I don't have any timestamps!"
910}"#;
911        let mut event = Annotated::<Event>::from_json(json).unwrap();
912
913        assert!(validate_event(&mut event, &EventValidationConfig::default()).is_err());
914        assert!(
915            validate_event(
916                &mut event,
917                &EventValidationConfig {
918                    is_validated: true,
919                    ..Default::default()
920                }
921            )
922            .is_ok()
923        );
924    }
925
926    #[test]
927    fn test_skip_event_timestamp_validation_on_renormalization() {
928        let json = r#"{
929  "event_id": "52df9022835246eeb317dbd739ccd059",
930  "transaction": "completely outdated transaction",
931  "start_timestamp": -2,
932  "timestamp": -1
933}"#;
934        let mut event = Annotated::<Event>::from_json(json).unwrap();
935
936        assert!(validate_event(&mut event, &EventValidationConfig::default()).is_err());
937        assert!(
938            validate_event(
939                &mut event,
940                &EventValidationConfig {
941                    is_validated: true,
942                    ..Default::default()
943                }
944            )
945            .is_ok()
946        );
947    }
948}