Skip to main content

relay_event_normalization/eap/
time.rs

1//! Time normalization for EAP items.
2
3use chrono::{DateTime, Utc};
4use relay_conventions::attributes::SENTRY__TIMESTAMP__SEQUENCE;
5use relay_event_schema::{
6    processor::{self, ProcessValue, ProcessingState},
7    protocol::{Attributes, OurLog, Replay, SpanV2, Timestamp, TraceMetric},
8};
9use relay_protocol::{Annotated, ErrorKind, Remark, RemarkType};
10use std::{fmt, time::Duration};
11
12use crate::ClockDriftProcessor;
13
14/// Error when the time is either too far in the future or past.
15#[derive(Debug, thiserror::Error, Clone, Copy)]
16pub struct TimestampOutOfRange {
17    timestamp: Timestamp,
18    received_at: DateTime<Utc>,
19    max_delta: Duration,
20    is_in_past: bool,
21}
22
23impl fmt::Display for TimestampOutOfRange {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        write!(
26            f,
27            "the item's timestamp ({}) is too far in the ",
28            self.timestamp
29        )?;
30        match self.is_in_past {
31            true => write!(f, "past ({})", self.received_at - self.max_delta)?,
32            false => write!(f, "future ({})", self.received_at + self.max_delta)?,
33        }
34        Ok(())
35    }
36}
37
38/// Configuration parameters for [`normalize`].
39#[derive(Debug, Default, Clone, Copy)]
40pub struct Config {
41    /// Apply a time sequence shift as provided by the SDK on the timestamp.
42    ///
43    /// This must only run once, to not shift the same timestamp multiple times and therefore should
44    /// be limited to processing Relays.
45    pub apply_sequence_shift: bool,
46    /// Timestamp when the item was received.
47    pub received_at: DateTime<Utc>,
48    /// Client local timestamp when the SDK sent the item.
49    pub sent_at: Option<DateTime<Utc>>,
50    /// Maximum amount of time the timestamp is allowed to be in the past.
51    pub max_in_past: Option<TimeEnforcement>,
52    /// Maximum amount of time the timestamp is allowed to be in the future.
53    pub max_in_future: Option<TimeEnforcement>,
54    /// Limits clock drift correction to a minimum duration.
55    pub minimum_clock_drift: Duration,
56}
57
58/// Configures how [`Config::max_in_past`] and [`Config::max_in_future`] should be enforced.
59#[derive(Copy, Clone, Debug)]
60pub enum TimeEnforcement {
61    /// The timestamp is shifted to comply with the given max.
62    Shift(Duration),
63    /// The item is rejected.
64    Reject(Duration),
65}
66
67impl TimeEnforcement {
68    fn shift(&self) -> Option<Duration> {
69        match self {
70            Self::Shift(duration) => Some(*duration),
71            _ => None,
72        }
73    }
74}
75
76/// Normalizes and validates timestamps.
77///
78/// Applies a time shift correction to correct for time drift on clients, see [`ClockDriftProcessor`].
79/// Also makes sure timestamps are within boundaries defined by [`Config::max_in_past`] and
80/// [`Config::max_in_future`].
81pub fn normalize<T>(item: &mut Annotated<T>, config: Config) -> Result<(), TimestampOutOfRange>
82where
83    T: TimeNormalize,
84{
85    let received_at = config.received_at;
86    let mut sent_at = config.sent_at;
87    let mut error_kind = ErrorKind::ClockDrift;
88
89    let timestamp = item
90        .value_mut()
91        .as_mut()
92        .map(|t| t.reference_timestamp_mut())
93        .and_then(|ts| ts.value().copied());
94
95    if let Some(timestamp) = timestamp {
96        if config
97            .max_in_past
98            .and_then(|te| te.shift())
99            .is_some_and(|delta| timestamp < received_at - delta)
100        {
101            error_kind = ErrorKind::PastTimestamp;
102            sent_at = Some(timestamp.into_inner());
103        } else if config
104            .max_in_future
105            .and_then(|te| te.shift())
106            .is_some_and(|delta| timestamp > received_at + delta)
107        {
108            error_kind = ErrorKind::FutureTimestamp;
109            sent_at = Some(timestamp.into_inner());
110        }
111    }
112
113    let mut processor = ClockDriftProcessor::new(sent_at, received_at)
114        .at_least(config.minimum_clock_drift)
115        .error_kind(error_kind);
116
117    if processor.is_drifted() {
118        let _ = processor::process_value(item, &mut processor, ProcessingState::root());
119        if let Some(item) = item.value_mut() {
120            processor.apply_correction_meta(item.reference_timestamp_mut().meta_mut());
121        }
122    }
123
124    let sequence = item
125        .value()
126        .and_then(|t| t.timestamp_sequence())
127        .filter(|d| *d > 0);
128
129    let timestamp = item
130        .value_mut()
131        .as_mut()
132        .map(|t| t.reference_timestamp_mut());
133
134    if let Some(timestamp) = timestamp.as_ref().and_then(|ts| ts.value()).copied() {
135        if let Some(TimeEnforcement::Reject(max_in_past)) = config.max_in_past
136            && timestamp < received_at - max_in_past
137        {
138            return Err(TimestampOutOfRange {
139                timestamp,
140                received_at,
141                max_delta: max_in_past,
142                is_in_past: true,
143            });
144        }
145
146        if let Some(TimeEnforcement::Reject(max_in_future)) = config.max_in_future
147            && timestamp > received_at + max_in_future
148        {
149            return Err(TimestampOutOfRange {
150                timestamp,
151                received_at,
152                max_delta: max_in_future,
153                is_in_past: false,
154            });
155        }
156    }
157
158    if config.apply_sequence_shift
159        && let Some(sequence) = sequence
160        && let Some(ts) = timestamp
161        && let Some(ts_value) = ts.value_mut()
162    {
163        // Always unconditionally apply the time-shift, this puts us potentially slightly over `max_in_future`,
164        // by up to ~5s, but this is preferable over losing the ordering.
165        ts_value.0 += chrono::TimeDelta::nanoseconds(sequence.into());
166        ts.meta_mut()
167            .add_remark(Remark::new(RemarkType::Substituted, "timestamp.sequence"));
168    }
169
170    Ok(())
171}
172
173/// Items which can be processed by [`normalize`].
174pub trait TimeNormalize: ProcessValue {
175    /// The base, reference timestamp of the item used for time shifts.
176    ///
177    /// Represents the timestamp when the item was created.
178    fn reference_timestamp_mut(&mut self) -> &mut Annotated<Timestamp>;
179
180    /// A tie breaker sent from SDKs for timestamps.
181    ///
182    /// This is usually stored in [`SENTRY__TIMESTAMP__SEQUENCE`] and applied as additional
183    /// nanoseconds to the timestamp.
184    fn timestamp_sequence(&self) -> Option<u32>;
185}
186
187impl TimeNormalize for OurLog {
188    fn reference_timestamp_mut(&mut self) -> &mut Annotated<Timestamp> {
189        &mut self.timestamp
190    }
191
192    fn timestamp_sequence(&self) -> Option<u32> {
193        get_timestamp_sequence(&self.attributes)
194    }
195}
196
197impl TimeNormalize for SpanV2 {
198    fn reference_timestamp_mut(&mut self) -> &mut Annotated<Timestamp> {
199        &mut self.start_timestamp
200    }
201
202    fn timestamp_sequence(&self) -> Option<u32> {
203        // Not supported for spans.
204        //
205        // If this ever becomes necessary to add, extra care must be taken to not create invalid
206        // spans where the start timestamp is moved after the end timestamp.
207        None
208    }
209}
210
211impl TimeNormalize for TraceMetric {
212    fn reference_timestamp_mut(&mut self) -> &mut Annotated<Timestamp> {
213        &mut self.timestamp
214    }
215
216    fn timestamp_sequence(&self) -> Option<u32> {
217        get_timestamp_sequence(&self.attributes)
218    }
219}
220
221impl TimeNormalize for Replay {
222    fn reference_timestamp_mut(&mut self) -> &mut Annotated<Timestamp> {
223        &mut self.timestamp
224    }
225
226    fn timestamp_sequence(&self) -> Option<u32> {
227        None
228    }
229}
230
231fn get_timestamp_sequence(attributes: &Annotated<Attributes>) -> Option<u32> {
232    attributes
233        .value()
234        .and_then(|attrs| attrs.get_value(SENTRY__TIMESTAMP__SEQUENCE))
235        .and_then(|v| v.as_f64())
236        .map(|v| v as _)
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    use relay_event_schema::processor::ProcessValue;
244    use relay_protocol::{
245        Annotated, Empty, FromValue, IntoValue, assert_annotated_snapshot, get_value,
246    };
247
248    #[derive(Debug, Clone, FromValue, IntoValue, Empty, ProcessValue)]
249    struct TestItem {
250        base: Annotated<Timestamp>,
251        other: Annotated<Timestamp>,
252    }
253
254    impl TimeNormalize for TestItem {
255        fn reference_timestamp_mut(&mut self) -> &mut Annotated<Timestamp> {
256            &mut self.base
257        }
258
259        fn timestamp_sequence(&self) -> Option<u32> {
260            Some(123)
261        }
262    }
263
264    fn ts(secs: i64) -> Timestamp {
265        Timestamp(DateTime::from_timestamp_secs(secs).unwrap())
266    }
267
268    #[test]
269    fn test_normalize_time_no_drift() {
270        let mut item = Annotated::new(TestItem {
271            base: ts(1_000).into(),
272            other: ts(1_010).into(),
273        });
274
275        let config = Config {
276            received_at: ts(1_100).0,
277            ..Default::default()
278        };
279
280        normalize(&mut item, config).unwrap();
281
282        assert_annotated_snapshot!(item, @r#"
283        {
284          "base": 1000.0,
285          "other": 1010.0
286        }
287        "#);
288    }
289
290    #[test]
291    fn test_normalize_time_client_drift() {
292        let mut item = Annotated::new(TestItem {
293            base: ts(50_000).into(),
294            other: ts(50_010).into(),
295        });
296
297        let config = Config {
298            sent_at: Some(ts(0).0),
299            received_at: ts(51_000).0,
300            ..Default::default()
301        };
302
303        normalize(&mut item, config).unwrap();
304
305        assert_annotated_snapshot!(item, @r#"
306        {
307          "base": 101000.0,
308          "other": 101010.0,
309          "_meta": {
310            "base": {
311              "": {
312                "err": [
313                  [
314                    "clock_drift",
315                    {
316                      "sdk_time": "1970-01-01T00:00:00+00:00",
317                      "server_time": "1970-01-01T14:10:00+00:00"
318                    }
319                  ]
320                ]
321              }
322            }
323          }
324        }
325        "#);
326    }
327
328    #[test]
329    fn test_normalize_time_too_far_in_past() {
330        let mut item = Annotated::new(TestItem {
331            base: ts(90_000).into(),
332            other: ts(80_000).into(),
333        });
334
335        let config = Config {
336            received_at: ts(100_000).0,
337            max_in_past: Some(TimeEnforcement::Shift(Duration::from_secs(10))),
338            ..Default::default()
339        };
340
341        normalize(&mut item, config).unwrap();
342
343        assert_annotated_snapshot!(item, @r#"
344        {
345          "base": 100000.0,
346          "other": 90000.0,
347          "_meta": {
348            "base": {
349              "": {
350                "err": [
351                  [
352                    "past_timestamp",
353                    {
354                      "sdk_time": "1970-01-02T01:00:00+00:00",
355                      "server_time": "1970-01-02T03:46:40+00:00"
356                    }
357                  ]
358                ]
359              }
360            }
361          }
362        }
363        "#);
364    }
365
366    #[test]
367    fn test_normalize_time_too_far_in_future() {
368        let mut item = Annotated::new(TestItem {
369            base: ts(90_000).into(),
370            other: ts(80_000).into(),
371        });
372
373        let config = Config {
374            received_at: ts(10_000).0,
375            max_in_future: Some(TimeEnforcement::Shift(Duration::from_secs(10))),
376            ..Default::default()
377        };
378
379        normalize(&mut item, config).unwrap();
380
381        assert_annotated_snapshot!(item, @r#"
382        {
383          "base": 10000.0,
384          "other": 0.0,
385          "_meta": {
386            "base": {
387              "": {
388                "err": [
389                  [
390                    "future_timestamp",
391                    {
392                      "sdk_time": "1970-01-02T01:00:00+00:00",
393                      "server_time": "1970-01-01T02:46:40+00:00"
394                    }
395                  ]
396                ]
397              }
398            }
399          }
400        }
401        "#);
402    }
403
404    #[test]
405    fn test_normalize_time_sequence_shift() {
406        let mut item = Annotated::new(TestItem {
407            base: ts(90_000).into(),
408            other: ts(80_000).into(),
409        });
410
411        let config = Config {
412            apply_sequence_shift: true,
413            ..Default::default()
414        };
415
416        normalize(&mut item, config).unwrap();
417
418        insta::assert_json_snapshot!(IntoValue::extract_meta_tree(&item), @r#"
419        {
420          "base": {
421            "": {
422              "rem": [
423                [
424                  "timestamp.sequence",
425                  "s"
426                ]
427              ]
428            }
429          }
430        }
431        "#);
432
433        // Need to assert the raw values instead of a snapshot because the serialization format of
434        // `Timestamp` is not precise enough for nanosecond precision.
435        assert_eq!(
436            get_value!(item.base!).0,
437            DateTime::from_timestamp_secs(90_000).unwrap() + chrono::TimeDelta::nanoseconds(123)
438        );
439        assert_eq!(
440            get_value!(item.other!).0,
441            DateTime::from_timestamp_secs(80_000).unwrap()
442        );
443    }
444
445    #[test]
446    fn test_normalize_time_sequence_shift_and_correction() {
447        let mut item = Annotated::new(TestItem {
448            base: ts(90_000).into(),
449            other: ts(80_000).into(),
450        });
451
452        let config = Config {
453            apply_sequence_shift: true,
454            received_at: ts(10_000).0,
455            max_in_future: Some(TimeEnforcement::Shift(Duration::from_secs(10))),
456            ..Default::default()
457        };
458
459        normalize(&mut item, config).unwrap();
460
461        insta::assert_json_snapshot!(IntoValue::extract_meta_tree(&item), @r#"
462        {
463          "base": {
464            "": {
465              "rem": [
466                [
467                  "timestamp.sequence",
468                  "s"
469                ]
470              ],
471              "err": [
472                [
473                  "future_timestamp",
474                  {
475                    "sdk_time": "1970-01-02T01:00:00+00:00",
476                    "server_time": "1970-01-01T02:46:40+00:00"
477                  }
478                ]
479              ]
480            }
481          }
482        }
483        "#);
484
485        assert_eq!(
486            get_value!(item.base!).0,
487            DateTime::from_timestamp_secs(10_000).unwrap() + chrono::TimeDelta::nanoseconds(123)
488        );
489        assert_eq!(
490            get_value!(item.other!).0,
491            DateTime::from_timestamp_secs(0).unwrap()
492        );
493    }
494
495    #[test]
496    fn test_normalize_time_too_far_in_past_reject() {
497        let mut item = Annotated::new(TestItem {
498            base: ts(90_000).into(),
499            other: ts(80_000).into(),
500        });
501
502        let config = Config {
503            received_at: ts(100_000).0,
504            max_in_past: Some(TimeEnforcement::Reject(Duration::from_secs(10))),
505            ..Default::default()
506        };
507
508        let err = normalize(&mut item, config).unwrap_err();
509        assert_eq!(
510            err.to_string(),
511            "the item's timestamp (1970-01-02 01:00:00 UTC) is too far in the past (1970-01-02 03:46:30 UTC)"
512        );
513    }
514
515    #[test]
516    fn test_normalize_time_too_far_in_future_reject() {
517        let mut item = Annotated::new(TestItem {
518            base: ts(90_000).into(),
519            other: ts(80_000).into(),
520        });
521
522        let config = Config {
523            received_at: ts(10_000).0,
524            max_in_future: Some(TimeEnforcement::Reject(Duration::from_secs(10))),
525            ..Default::default()
526        };
527
528        let err = normalize(&mut item, config).unwrap_err();
529        assert_eq!(
530            err.to_string(),
531            "the item's timestamp (1970-01-02 01:00:00 UTC) is too far in the future (1970-01-01 02:46:50 UTC)"
532        );
533    }
534}