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