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            && *timestamp > received_at + Duration::seconds(secs)
104        {
105            error_kind = ErrorKind::FutureTimestamp;
106            sent_at = Some(*timestamp);
107            return Ok(());
108        }
109
110        if let Some(secs) = max_secs_in_past
111            && *timestamp < received_at - Duration::seconds(secs)
112        {
113            error_kind = ErrorKind::PastTimestamp;
114            sent_at = Some(*timestamp);
115            return Ok(());
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            && span.timestamp.value().is_none()
225        {
226            // event timestamp guaranteed to be `Some` due to validate_transaction call
227            span.timestamp.set_value(event.timestamp.value().cloned());
228            span.status = Annotated::new(relay_base_schema::spans::SpanStatus::DeadlineExceeded);
229        }
230    }
231}
232
233/// Validates the spans in the transaction.
234///
235/// A [`ProcessingResult`] error is returned if there's an invalid span. For
236/// span validity, see [`validate_span`].
237fn validate_spans(
238    transaction: &Event,
239    timestamp_range: Option<&Range<UnixTimestamp>>,
240) -> ProcessingResult {
241    let Some(spans) = transaction.spans.value() else {
242        return Ok(());
243    };
244
245    for span in spans {
246        if let Some(span) = span.value() {
247            validate_span(span, timestamp_range)?;
248        } else {
249            return Err(ProcessingAction::InvalidTransaction(
250                "spans must be valid in transaction event",
251            ));
252        }
253    }
254
255    Ok(())
256}
257
258/// Validates a span.
259///
260/// A [`ProcessingResult`] error is returned when the span is invalid. A span is
261/// valid if all the following conditions are met:
262/// - A start timestamp exists.
263/// - An end timestamp exists.
264/// - Start timestamp must be no later than end timestamp.
265/// - A trace id exists.
266/// - A span id exists.
267pub fn validate_span(
268    span: &Span,
269    timestamp_range: Option<&Range<UnixTimestamp>>,
270) -> ProcessingResult {
271    match (span.start_timestamp.value(), span.timestamp.value()) {
272        (Some(start), Some(end)) => {
273            validate_timestamps(start, end, timestamp_range)?;
274        }
275        (_, None) => {
276            // XXX: Maybe do the same as event.timestamp?
277            return Err(ProcessingAction::InvalidTransaction(
278                "span is missing timestamp",
279            ));
280        }
281        (None, _) => {
282            // XXX: Maybe copy timestamp over?
283            return Err(ProcessingAction::InvalidTransaction(
284                "span is missing start_timestamp",
285            ));
286        }
287    }
288
289    if span.trace_id.value().is_none() {
290        return Err(ProcessingAction::InvalidTransaction(
291            "span is missing trace_id",
292        ));
293    }
294
295    if span.span_id.value().is_none() {
296        return Err(ProcessingAction::InvalidTransaction(
297            "span is missing span_id",
298        ));
299    }
300
301    Ok(())
302}
303
304/// Validate fields that go into a `sentry.models.BoundedIntegerField`.
305fn validate_bounded_integer_field(value: u64) -> ProcessingResult {
306    if value < 2_147_483_647 {
307        Ok(())
308    } else {
309        Err(ProcessingAction::DeleteValueHard)
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use chrono::TimeZone;
316    use relay_base_schema::spans::SpanStatus;
317    use relay_event_schema::protocol::Contexts;
318    use relay_protocol::{Object, get_value};
319
320    use super::*;
321
322    fn new_test_event() -> Annotated<Event> {
323        let start = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
324        let end = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap();
325        Annotated::new(Event {
326            ty: Annotated::new(EventType::Transaction),
327            transaction: Annotated::new("/".to_owned()),
328            start_timestamp: Annotated::new(start.into()),
329            timestamp: Annotated::new(end.into()),
330            contexts: {
331                let mut contexts = Contexts::new();
332                contexts.add(TraceContext {
333                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
334                    span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
335                    op: Annotated::new("http.server".to_owned()),
336                    ..Default::default()
337                });
338                Annotated::new(contexts)
339            },
340            spans: Annotated::new(vec![Annotated::new(Span {
341                start_timestamp: Annotated::new(start.into()),
342                timestamp: Annotated::new(end.into()),
343                trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
344                span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
345                op: Annotated::new("db.statement".to_owned()),
346                ..Default::default()
347            })]),
348            ..Default::default()
349        })
350    }
351
352    #[test]
353    fn test_timestamp_added_if_missing() {
354        let mut event = Annotated::new(Event::default());
355        assert!(get_value!(event.timestamp).is_none());
356        assert!(validate_event(&mut event, &EventValidationConfig::default()).is_ok());
357        assert!(get_value!(event.timestamp).is_some());
358    }
359
360    #[test]
361    fn test_discards_when_timestamp_out_of_range() {
362        let mut event = new_test_event();
363
364        assert!(matches!(
365            validate_event(
366                &mut event,
367                &EventValidationConfig {
368                    transaction_timestamp_range: Some(UnixTimestamp::now()..UnixTimestamp::now()),
369                    is_validated: false,
370                    ..Default::default()
371                }
372            ),
373            Err(ProcessingAction::InvalidTransaction(
374                "timestamp is out of the valid range for metrics"
375            ))
376        ));
377    }
378
379    #[test]
380    fn test_discards_when_missing_start_timestamp() {
381        let mut event = Annotated::new(Event {
382            ty: Annotated::new(EventType::Transaction),
383            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
384            ..Default::default()
385        });
386
387        assert_eq!(
388            validate_event(&mut event, &EventValidationConfig::default()),
389            Err(ProcessingAction::InvalidTransaction(
390                "start_timestamp hard-required for transaction events"
391            ))
392        );
393    }
394
395    #[test]
396    fn test_discards_on_missing_contexts_map() {
397        let mut event = Annotated::new(Event {
398            ty: Annotated::new(EventType::Transaction),
399            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
400            start_timestamp: Annotated::new(
401                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
402            ),
403            ..Default::default()
404        });
405
406        assert_eq!(
407            validate_event(
408                &mut event,
409                &EventValidationConfig {
410                    transaction_timestamp_range: None,
411                    is_validated: false,
412                    ..Default::default()
413                }
414            ),
415            Err(ProcessingAction::InvalidTransaction(
416                "missing valid trace context"
417            ))
418        );
419    }
420
421    #[test]
422    fn test_discards_on_missing_context() {
423        let mut event = Annotated::new(Event {
424            ty: Annotated::new(EventType::Transaction),
425            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
426            start_timestamp: Annotated::new(
427                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
428            ),
429            contexts: Annotated::new(Contexts::new()),
430            ..Default::default()
431        });
432
433        assert_eq!(
434            validate_event(
435                &mut event,
436                &EventValidationConfig {
437                    transaction_timestamp_range: None,
438                    is_validated: false,
439                    ..Default::default()
440                }
441            ),
442            Err(ProcessingAction::InvalidTransaction(
443                "missing valid trace context"
444            ))
445        );
446    }
447
448    #[test]
449    fn test_discards_on_null_context() {
450        let mut event = Annotated::new(Event {
451            ty: Annotated::new(EventType::Transaction),
452            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
453            start_timestamp: Annotated::new(
454                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
455            ),
456            contexts: Annotated::new(Contexts({
457                let mut contexts = Object::new();
458                contexts.insert("trace".to_owned(), Annotated::empty());
459                contexts
460            })),
461            ..Default::default()
462        });
463
464        assert_eq!(
465            validate_event(&mut event, &EventValidationConfig::default()),
466            Err(ProcessingAction::InvalidTransaction(
467                "missing valid trace context"
468            ))
469        );
470    }
471
472    #[test]
473    fn test_discards_on_missing_trace_id_in_context() {
474        let mut event = Annotated::new(Event {
475            ty: Annotated::new(EventType::Transaction),
476            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
477            start_timestamp: Annotated::new(
478                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
479            ),
480            contexts: {
481                let mut contexts = Contexts::new();
482                contexts.add(TraceContext::default());
483                Annotated::new(contexts)
484            },
485            ..Default::default()
486        });
487
488        assert_eq!(
489            validate_event(&mut event, &EventValidationConfig::default()),
490            Err(ProcessingAction::InvalidTransaction(
491                "trace context is missing trace_id"
492            ))
493        );
494    }
495
496    #[test]
497    fn test_discards_on_missing_span_id_in_context() {
498        let mut event = Annotated::new(Event {
499            ty: Annotated::new(EventType::Transaction),
500            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
501            start_timestamp: Annotated::new(
502                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
503            ),
504            contexts: {
505                let mut contexts = Contexts::new();
506                contexts.add(TraceContext {
507                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
508                    ..Default::default()
509                });
510                Annotated::new(contexts)
511            },
512            ..Default::default()
513        });
514
515        assert_eq!(
516            validate_event(&mut event, &EventValidationConfig::default()),
517            Err(ProcessingAction::InvalidTransaction(
518                "trace context is missing span_id"
519            ))
520        );
521    }
522
523    #[test]
524    fn test_discards_transaction_event_with_nulled_out_span() {
525        let mut event = Annotated::new(Event {
526            ty: Annotated::new(EventType::Transaction),
527            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
528            start_timestamp: Annotated::new(
529                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
530            ),
531            contexts: {
532                let mut contexts = Contexts::new();
533                contexts.add(TraceContext {
534                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
535                    span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
536                    op: Annotated::new("http.server".to_owned()),
537                    ..Default::default()
538                });
539                Annotated::new(contexts)
540            },
541            spans: Annotated::new(vec![Annotated::empty()]),
542            ..Default::default()
543        });
544
545        assert_eq!(
546            validate_event(&mut event, &EventValidationConfig::default()),
547            Err(ProcessingAction::InvalidTransaction(
548                "spans must be valid in transaction event"
549            ))
550        );
551    }
552
553    #[test]
554    fn test_discards_transaction_event_with_span_with_missing_start_timestamp() {
555        let mut event = Annotated::new(Event {
556            ty: Annotated::new(EventType::Transaction),
557            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
558            start_timestamp: Annotated::new(
559                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
560            ),
561            contexts: {
562                let mut contexts = Contexts::new();
563                contexts.add(TraceContext {
564                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
565                    span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
566                    op: Annotated::new("http.server".to_owned()),
567                    ..Default::default()
568                });
569                Annotated::new(contexts)
570            },
571            spans: Annotated::new(vec![Annotated::new(Span {
572                timestamp: Annotated::new(
573                    Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
574                ),
575                ..Default::default()
576            })]),
577            ..Default::default()
578        });
579
580        assert_eq!(
581            validate_event(&mut event, &EventValidationConfig::default()),
582            Err(ProcessingAction::InvalidTransaction(
583                "span is missing start_timestamp"
584            ))
585        );
586    }
587
588    #[test]
589    fn test_discards_transaction_event_with_span_with_missing_trace_id() {
590        let mut event = Annotated::new(Event {
591            ty: Annotated::new(EventType::Transaction),
592            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
593            start_timestamp: Annotated::new(
594                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
595            ),
596            contexts: {
597                let mut contexts = Contexts::new();
598                contexts.add(TraceContext {
599                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
600                    span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
601                    op: Annotated::new("http.server".to_owned()),
602                    ..Default::default()
603                });
604                Annotated::new(contexts)
605            },
606            spans: Annotated::new(vec![Annotated::new(Span {
607                timestamp: Annotated::new(
608                    Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
609                ),
610                start_timestamp: Annotated::new(
611                    Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
612                ),
613                ..Default::default()
614            })]),
615            ..Default::default()
616        });
617
618        assert_eq!(
619            validate_event(&mut event, &EventValidationConfig::default()),
620            Err(ProcessingAction::InvalidTransaction(
621                "span is missing trace_id"
622            ))
623        );
624    }
625
626    #[test]
627    fn test_discards_transaction_event_with_span_with_missing_span_id() {
628        let mut event = Annotated::new(Event {
629            ty: Annotated::new(EventType::Transaction),
630            timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
631            start_timestamp: Annotated::new(
632                Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
633            ),
634            contexts: {
635                let mut contexts = Contexts::new();
636                contexts.add(TraceContext {
637                    trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
638                    span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
639                    op: Annotated::new("http.server".to_owned()),
640                    ..Default::default()
641                });
642                Annotated::new(contexts)
643            },
644            spans: Annotated::new(vec![Annotated::new(Span {
645                timestamp: Annotated::new(
646                    Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
647                ),
648                start_timestamp: Annotated::new(
649                    Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
650                ),
651                trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
652                ..Default::default()
653            })]),
654            ..Default::default()
655        });
656
657        assert_eq!(
658            validate_event(&mut event, &EventValidationConfig::default()),
659            Err(ProcessingAction::InvalidTransaction(
660                "span is missing span_id"
661            ))
662        );
663    }
664
665    #[test]
666    fn test_reject_stale_transaction() {
667        let json = r#"{
668  "event_id": "52df9022835246eeb317dbd739ccd059",
669  "start_timestamp": -2,
670  "timestamp": -1
671}"#;
672        let mut transaction = Annotated::<Event>::from_json(json).unwrap();
673        let res = validate_event(&mut transaction, &EventValidationConfig::default());
674        assert_eq!(
675            res.unwrap_err().to_string(),
676            "invalid transaction event: timestamp is too stale"
677        );
678    }
679
680    /// Test that timestamp normalization updates a transaction's timestamps to
681    /// be acceptable, when both timestamps are similarly stale.
682    #[test]
683    fn test_accept_recent_transactions_with_stale_timestamps() {
684        let config = EventValidationConfig {
685            received_at: Some(Utc::now()),
686            max_secs_in_past: Some(2),
687            max_secs_in_future: Some(1),
688            is_validated: false,
689            ..Default::default()
690        };
691
692        let json = r#"{
693  "event_id": "52df9022835246eeb317dbd739ccd059",
694  "transaction": "I have a stale timestamp, but I'm recent!",
695  "start_timestamp": -2,
696  "timestamp": -1
697}"#;
698        let mut event = Annotated::<Event>::from_json(json).unwrap();
699
700        assert!(validate_event(&mut event, &config).is_ok());
701    }
702
703    /// Test that transactions are rejected as invalid when timestamp normalization isn't enough.
704    ///
705    /// When the end timestamp is recent but the start timestamp is stale, timestamp normalization
706    /// will fix the timestamps based on the end timestamp. The start timestamp will be more recent,
707    /// but not recent enough for the transaction to be accepted. The transaction will be rejected.
708    #[test]
709    fn test_reject_stale_transactions_after_timestamp_normalization() {
710        let now = Utc::now();
711        let config = EventValidationConfig {
712            received_at: Some(now),
713            max_secs_in_past: Some(2),
714            max_secs_in_future: Some(1),
715            is_validated: false,
716            ..Default::default()
717        };
718
719        let json = format!(
720            r#"{{
721          "event_id": "52df9022835246eeb317dbd739ccd059",
722          "transaction": "clockdrift is not enough to accept me :(",
723          "start_timestamp": -62135811111,
724          "timestamp": {}
725        }}"#,
726            now.timestamp()
727        );
728        let mut event = Annotated::<Event>::from_json(json.as_str()).unwrap();
729
730        assert_eq!(
731            validate_event(&mut event, &config).unwrap_err().to_string(),
732            "invalid transaction event: timestamp is too stale"
733        );
734    }
735
736    /// Validates an unfinished span in a transaction, and the transaction is accepted.
737    #[test]
738    fn test_accept_transactions_with_unfinished_spans() {
739        let json = r#"{
740  "event_id": "52df9022835246eeb317dbd739ccd059",
741  "type": "transaction",
742  "transaction": "I have a stale timestamp, but I'm recent!",
743  "start_timestamp": 1,
744  "timestamp": 2,
745  "contexts": {
746    "trace": {
747      "trace_id": "ff62a8b040f340bda5d830223def1d81",
748      "span_id": "bd429c44b67a3eb4"
749    }
750  },
751  "spans": [
752    {
753      "span_id": "bd429c44b67a3eb4",
754      "start_timestamp": 1,
755      "timestamp": null,
756      "trace_id": "ff62a8b040f340bda5d830223def1d81"
757    }
758  ]
759}"#;
760        let mut event = Annotated::<Event>::from_json(json).unwrap();
761
762        assert!(validate_event(&mut event, &EventValidationConfig::default()).is_ok());
763
764        let event = get_value!(event!);
765        let spans = &event.spans;
766        let span = get_value!(spans[0]!);
767
768        assert_eq!(span.timestamp, event.timestamp);
769        assert_eq!(span.status.value().unwrap(), &SpanStatus::DeadlineExceeded);
770    }
771
772    /// Validates an unfinished span is finished with the normalized transaction's timestamp.
773    #[test]
774    fn test_finish_spans_with_normalized_transaction_end_timestamp() {
775        let json = r#"{
776  "event_id": "52df9022835246eeb317dbd739ccd059",
777  "type": "transaction",
778  "transaction": "I have a stale timestamp, but I'm recent!",
779  "start_timestamp": 946731000,
780  "timestamp": 946731555,
781  "contexts": {
782    "trace": {
783      "trace_id": "ff62a8b040f340bda5d830223def1d81",
784      "span_id": "bd429c44b67a3eb4"
785    }
786  },
787  "spans": [
788    {
789      "span_id": "bd429c44b67a3eb4",
790      "start_timestamp": 946731000,
791      "timestamp": null,
792      "trace_id": "ff62a8b040f340bda5d830223def1d81"
793    }
794  ]
795}"#;
796        let mut event = Annotated::<Event>::from_json(json).unwrap();
797
798        validate_event(
799            &mut event,
800            &EventValidationConfig {
801                received_at: Some(Utc::now()),
802                max_secs_in_past: Some(2),
803                max_secs_in_future: Some(1),
804                is_validated: false,
805                ..Default::default()
806            },
807        )
808        .unwrap();
809        validate_event(&mut event, &EventValidationConfig::default()).unwrap();
810
811        let event = get_value!(event!);
812        let spans = &event.spans;
813        let span = get_value!(spans[0]!);
814
815        assert_eq!(span.timestamp.value(), event.timestamp.value());
816    }
817
818    #[test]
819    fn test_skip_transaction_validation_on_renormalization() {
820        let json = r#"{
821  "event_id": "52df9022835246eeb317dbd739ccd059",
822  "type": "transaction",
823  "transaction": "I'm invalid because I don't have any timestamps!"
824}"#;
825        let mut event = Annotated::<Event>::from_json(json).unwrap();
826
827        assert!(validate_event(&mut event, &EventValidationConfig::default()).is_err());
828        assert!(
829            validate_event(
830                &mut event,
831                &EventValidationConfig {
832                    is_validated: true,
833                    ..Default::default()
834                }
835            )
836            .is_ok()
837        );
838    }
839
840    #[test]
841    fn test_skip_event_timestamp_validation_on_renormalization() {
842        let json = r#"{
843  "event_id": "52df9022835246eeb317dbd739ccd059",
844  "transaction": "completely outdated transaction",
845  "start_timestamp": -2,
846  "timestamp": -1
847}"#;
848        let mut event = Annotated::<Event>::from_json(json).unwrap();
849
850        assert!(validate_event(&mut event, &EventValidationConfig::default()).is_err());
851        assert!(
852            validate_event(
853                &mut event,
854                &EventValidationConfig {
855                    is_validated: true,
856                    ..Default::default()
857                }
858            )
859            .is_ok()
860        );
861    }
862}