relay_event_normalization/eap/
time.rs1use 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#[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#[derive(Debug, Default, Clone, Copy)]
40pub struct Config {
41 pub apply_sequence_shift: bool,
46 pub received_at: DateTime<Utc>,
48 pub sent_at: Option<DateTime<Utc>>,
50 pub max_in_past: Option<TimeEnforcement>,
52 pub max_in_future: Option<TimeEnforcement>,
54 pub minimum_clock_drift: Duration,
56}
57
58#[derive(Copy, Clone, Debug)]
60pub enum TimeEnforcement {
61 Shift(Duration),
63 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
76pub 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 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
173pub trait TimeNormalize: ProcessValue {
175 fn reference_timestamp_mut(&mut self) -> &mut Annotated<Timestamp>;
179
180 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 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 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}