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