1use std::net::IpAddr as StdIpAddr;
4
5use relay_event_schema::processor::{self, ProcessingState, Processor};
6use relay_event_schema::protocol::{Contexts, IpAddr, Replay};
7use relay_protocol::Annotated;
8
9use crate::event::normalize_user_geoinfo;
10use crate::normalize::user_agent;
11use crate::user_agent::RawUserAgentInfo;
12use crate::{GeoIpLookup, trimming};
13
14#[derive(Debug, thiserror::Error)]
18pub enum ReplayError {
19 #[error("missing replay_id")]
21 MissingReplayId,
22 #[error("missing segment_id")]
24 MissingSegmentId,
25 #[error("segment_id too large")]
27 SegmentIdTooLarge,
28 #[error("invalid error_id specified")]
30 InvalidErrorId,
31 #[error("invalid trace_id specified")]
33 InvalidTraceId,
34}
35
36pub fn validate(replay: &Replay) -> Result<(), ReplayError> {
41 replay
42 .replay_id
43 .value()
44 .ok_or(ReplayError::MissingReplayId)?;
45
46 let segment_id = *replay
47 .segment_id
48 .value()
49 .ok_or(ReplayError::MissingSegmentId)?;
50
51 if segment_id > u16::MAX as u64 {
52 return Err(ReplayError::SegmentIdTooLarge);
53 }
54
55 if replay
56 .error_ids
57 .value()
58 .into_iter()
59 .flat_map(|v| v.iter())
60 .any(|v| v.meta().has_errors())
61 {
62 return Err(ReplayError::InvalidErrorId);
63 }
64
65 if replay
66 .trace_ids
67 .value()
68 .into_iter()
69 .flat_map(|v| v.iter())
70 .any(|v| v.meta().has_errors())
71 {
72 return Err(ReplayError::InvalidTraceId);
73 }
74
75 Ok(())
76}
77
78pub fn normalize(
80 replay: &mut Annotated<Replay>,
81 client_ip: Option<StdIpAddr>,
82 user_agent: &RawUserAgentInfo<&str>,
83 geoip_lookup: &GeoIpLookup,
84) {
85 let _ = processor::apply(replay, |replay_value, meta| {
86 normalize_platform(replay_value);
87 normalize_ip_address(replay_value, client_ip);
88 normalize_user_geoinfo(
89 geoip_lookup,
90 &mut replay_value.user,
91 client_ip.map(|ip| IpAddr(ip.to_string())).as_ref(),
92 );
93 normalize_user_agent(replay_value, user_agent);
94 normalize_type(replay_value);
95 normalize_array_fields(replay_value);
96 let _ = trimming::TrimmingProcessor::new().process_replay(
97 replay_value,
98 meta,
99 ProcessingState::root(),
100 );
101 Ok(())
102 });
103}
104
105fn normalize_array_fields(replay: &mut Replay) {
106 if let Some(items) = replay.error_ids.value_mut() {
109 items.truncate(100);
110 }
111
112 if let Some(items) = replay.trace_ids.value_mut() {
113 items.truncate(100);
114 }
115
116 if let Some(items) = replay.urls.value_mut() {
117 items.truncate(100);
118 }
119 if let Some(items) = replay.segment_names.value_mut() {
120 items.truncate(100);
121 }
122}
123
124fn normalize_ip_address(replay: &mut Replay, ip_address: Option<StdIpAddr>) {
125 crate::event::normalize_ip_addresses(
126 &mut replay.request,
127 &mut replay.user,
128 replay.platform.as_str(),
129 ip_address.map(|ip| IpAddr(ip.to_string())).as_ref(),
130 replay.sdk.value(),
131 );
132}
133
134fn normalize_user_agent(replay: &mut Replay, default_user_agent: &RawUserAgentInfo<&str>) {
135 let headers = match replay
136 .request
137 .value()
138 .and_then(|request| request.headers.value())
139 {
140 Some(headers) => headers,
141 None => return,
142 };
143
144 let user_agent_info = RawUserAgentInfo::from_headers(headers);
145 let user_agent_info = if user_agent_info.is_empty() {
146 default_user_agent
147 } else {
148 &user_agent_info
149 };
150
151 let contexts = replay.contexts.get_or_insert_with(Contexts::new);
152 user_agent::normalize_user_agent_info_generic(contexts, &replay.platform, user_agent_info);
153}
154
155fn normalize_platform(replay: &mut Replay) {
156 let platform = replay.platform.get_or_insert_with(|| "other".to_owned());
158
159 if !crate::is_valid_platform(platform) {
161 replay.platform = Annotated::from("other".to_owned());
162 }
163}
164
165fn normalize_type(replay: &mut Replay) {
166 replay.ty = Annotated::from("replay_event".to_owned());
167}
168
169#[cfg(test)]
170mod tests {
171 use std::net::{IpAddr, Ipv4Addr};
172
173 use chrono::{TimeZone, Utc};
174 use insta::assert_json_snapshot;
175 use relay_protocol::{SerializableAnnotated, assert_annotated_snapshot, get_value};
176 use uuid::Uuid;
177
178 use relay_event_schema::protocol::{
179 BrowserContext, Context, DeviceContext, EventId, OsContext, TagEntry, Tags,
180 };
181
182 use super::*;
183
184 #[test]
185 fn test_event_roundtrip() {
186 let json = r#"{
188 "event_id": "52df9022835246eeb317dbd739ccd059",
189 "replay_id": "52df9022835246eeb317dbd739ccd059",
190 "segment_id": 0,
191 "replay_type": "session",
192 "error_sample_rate": 0.5,
193 "session_sample_rate": 0.5,
194 "timestamp": 946684800.0,
195 "replay_start_timestamp": 946684800.0,
196 "urls": ["localhost:9000"],
197 "error_ids": ["52df9022835246eeb317dbd739ccd059"],
198 "trace_ids": ["52df9022835246eeb317dbd739ccd059"],
199 "platform": "myplatform",
200 "release": "myrelease",
201 "dist": "mydist",
202 "environment": "myenv",
203 "tags": [
204 [
205 "tag",
206 "value"
207 ]
208 ]
209}"#;
210
211 let replay = Annotated::new(Replay {
212 event_id: Annotated::new(EventId("52df9022835246eeb317dbd739ccd059".parse().unwrap())),
213 replay_id: Annotated::new(EventId("52df9022835246eeb317dbd739ccd059".parse().unwrap())),
214 replay_type: Annotated::new("session".to_owned()),
215 segment_id: Annotated::new(0),
216 timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
217 replay_start_timestamp: Annotated::new(
218 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
219 ),
220 urls: Annotated::new(vec![Annotated::new("localhost:9000".to_owned())]),
221 error_ids: Annotated::new(vec![Annotated::new(
222 Uuid::parse_str("52df9022835246eeb317dbd739ccd059").unwrap(),
223 )]),
224 trace_ids: Annotated::new(vec![Annotated::new(
225 Uuid::parse_str("52df9022835246eeb317dbd739ccd059").unwrap(),
226 )]),
227 platform: Annotated::new("myplatform".to_owned()),
228 release: Annotated::new("myrelease".to_owned().into()),
229 dist: Annotated::new("mydist".to_owned()),
230 environment: Annotated::new("myenv".to_owned()),
231 tags: {
232 let items = vec![Annotated::new(TagEntry(
233 Annotated::new("tag".to_owned()),
234 Annotated::new("value".to_owned()),
235 ))];
236 Annotated::new(Tags(items.into()))
237 },
238 ..Default::default()
239 });
240
241 assert_eq!(replay, Annotated::from_json(json).unwrap());
242 }
243
244 #[test]
245 fn test_lenient_release() {
246 let input = r#"{"release":42}"#;
247 let output = r#"{"release":"42"}"#;
248 let event = Annotated::new(Replay {
249 release: Annotated::new("42".to_owned().into()),
250 ..Default::default()
251 });
252
253 assert_eq!(event, Annotated::from_json(input).unwrap());
254 assert_eq!(output, event.to_json().unwrap());
255 }
256
257 #[test]
258 fn test_set_user_agent_meta() {
259 let payload = include_str!("../../tests/fixtures/replay.json");
261
262 let mut replay: Annotated<Replay> = Annotated::from_json(payload).unwrap();
263 normalize(
264 &mut replay,
265 None,
266 &RawUserAgentInfo::default(),
267 &GeoIpLookup::empty(),
268 );
269
270 let contexts = get_value!(replay.contexts!);
271 assert_eq!(
272 contexts.get::<BrowserContext>(),
273 Some(&BrowserContext {
274 name: Annotated::new("Safari".to_owned()),
275 version: Annotated::new("15.5".to_owned()),
276 ..Default::default()
277 })
278 );
279 assert_eq!(
280 contexts.get_key("client_os"),
281 Some(&Context::Os(Box::new(OsContext {
282 name: Annotated::new("Mac OS X".to_owned()),
283 version: Annotated::new(">=10.15.7".to_owned()),
284 ..Default::default()
285 })))
286 );
287 assert_eq!(
288 contexts.get::<DeviceContext>(),
289 Some(&DeviceContext {
290 family: Annotated::new("Mac".to_owned()),
291 brand: Annotated::new("Apple".to_owned()),
292 model: Annotated::new("Mac".to_owned()),
293 ..Default::default()
294 })
295 );
296 }
297
298 #[test]
299 fn test_missing_user() {
300 let payload = include_str!("../../tests/fixtures/replay_missing_user.json");
301
302 let mut replay: Annotated<Replay> = Annotated::from_json(payload).unwrap();
303
304 normalize(
306 &mut replay,
307 None,
308 &RawUserAgentInfo::default(),
309 &GeoIpLookup::empty(),
310 );
311 assert_eq!(get_value!(replay.user.geo), None);
312
313 let ip_address = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
315 normalize(
316 &mut replay,
317 Some(ip_address),
318 &RawUserAgentInfo::default(),
319 &GeoIpLookup::empty(),
320 );
321
322 let ipaddr = get_value!(replay.user!).ip_address.as_str();
323 assert_eq!(Some("127.0.0.1"), ipaddr);
324 }
325
326 #[test]
327 fn test_set_ip_address_missing_user_ip_address_and_geo() {
328 let lookup = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
329 let ip_address = IpAddr::V4(Ipv4Addr::new(2, 125, 160, 216));
330
331 let payload = include_str!("../../tests/fixtures/replay_missing_user_ip_address.json");
333
334 let mut replay: Annotated<Replay> = Annotated::from_json(payload).unwrap();
335 normalize(
336 &mut replay,
337 Some(ip_address),
338 &RawUserAgentInfo::default(),
339 &lookup,
340 );
341
342 let user = &replay.value().unwrap().user;
343 assert_json_snapshot!(SerializableAnnotated(user), @r###"
344 {
345 "id": "123",
346 "email": "user@site.com",
347 "ip_address": "2.125.160.216",
348 "username": "user",
349 "geo": {
350 "country_code": "GB",
351 "city": "Boxford",
352 "subdivision": "England",
353 "region": "United Kingdom"
354 }
355 }
356 "###);
357 }
358
359 #[test]
360 fn test_loose_type_requirements() {
361 let payload = include_str!("../../tests/fixtures/replay_failure_22_08_31.json");
362
363 let mut replay: Annotated<Replay> = Annotated::from_json(payload).unwrap();
364 normalize(
365 &mut replay,
366 None,
367 &RawUserAgentInfo::default(),
368 &GeoIpLookup::empty(),
369 );
370
371 let user = get_value!(replay.user!);
372 assert_eq!(user.ip_address.as_str(), Some("127.1.1.1"));
373 assert_eq!(user.username.value(), None);
374 assert_eq!(user.email.as_str(), Some("email@sentry.io"));
375 assert_eq!(user.id.as_str(), Some("1"));
376 }
377
378 #[test]
379 fn test_capped_values() {
380 let urls: Vec<Annotated<String>> = (0..101)
381 .map(|_| Annotated::new("localhost:9000".to_owned()))
382 .collect();
383
384 let error_ids: Vec<Annotated<Uuid>> = (0..101)
385 .map(|_| Annotated::new(Uuid::parse_str("52df9022835246eeb317dbd739ccd059").unwrap()))
386 .collect();
387
388 let trace_ids: Vec<Annotated<Uuid>> = (0..101)
389 .map(|_| Annotated::new(Uuid::parse_str("52df9022835246eeb317dbd739ccd059").unwrap()))
390 .collect();
391
392 let segment_names: Vec<Annotated<String>> = (0..101)
393 .map(|_| Annotated::new("/users/{id}".to_owned()))
394 .collect();
395
396 let mut replay = Annotated::new(Replay {
397 urls: Annotated::new(urls),
398 error_ids: Annotated::new(error_ids),
399 trace_ids: Annotated::new(trace_ids),
400 segment_names: Annotated::new(segment_names),
401 ..Default::default()
402 });
403
404 let replay_value = replay.value_mut().as_mut().unwrap();
405 normalize_array_fields(replay_value);
406
407 assert!(replay_value.error_ids.value().unwrap().len() == 100);
408 assert!(replay_value.trace_ids.value().unwrap().len() == 100);
409 assert!(replay_value.urls.value().unwrap().len() == 100);
410 assert!(replay_value.segment_names.value().unwrap().len() == 100);
411 }
412
413 #[test]
414 fn test_truncated_list_less_than_limit() {
415 let mut replay = Annotated::new(Replay {
416 urls: Annotated::new(Vec::new()),
417 error_ids: Annotated::new(Vec::new()),
418 trace_ids: Annotated::new(Vec::new()),
419 ..Default::default()
420 });
421
422 let replay_value = replay.value_mut().as_mut().unwrap();
423 normalize_array_fields(replay_value);
424
425 assert!(replay_value.error_ids.value().unwrap().is_empty());
426 assert!(replay_value.trace_ids.value().unwrap().is_empty());
427 assert!(replay_value.urls.value().unwrap().is_empty());
428 }
429
430 #[test]
431 fn test_error_id_validation() {
432 let json = r#"{
434 "event_id": "52df9022835246eeb317dbd739ccd059",
435 "replay_id": "52df9022835246eeb317dbd739ccd059",
436 "segment_id": 0,
437 "replay_type": "session",
438 "error_sample_rate": 0.5,
439 "session_sample_rate": 0.5,
440 "timestamp": 946684800.0,
441 "replay_start_timestamp": 946684800.0,
442 "urls": ["localhost:9000"],
443 "error_ids": ["test"],
444 "trace_ids": [],
445 "platform": "myplatform",
446 "release": "myrelease",
447 "dist": "mydist",
448 "environment": "myenv",
449 "tags": [
450 [
451 "tag",
452 "value"
453 ]
454 ]
455}"#;
456
457 let mut replay = Annotated::<Replay>::from_json(json).unwrap();
458 let validation_result = validate(replay.value_mut().as_mut().unwrap());
459 assert!(validation_result.is_err());
460 }
461
462 #[test]
463 fn test_trace_id_validation() {
464 let json = r#"{
466 "event_id": "52df9022835246eeb317dbd739ccd059",
467 "replay_id": "52df9022835246eeb317dbd739ccd059",
468 "segment_id": 0,
469 "replay_type": "session",
470 "error_sample_rate": 0.5,
471 "session_sample_rate": 0.5,
472 "timestamp": 946684800.0,
473 "replay_start_timestamp": 946684800.0,
474 "urls": ["localhost:9000"],
475 "error_ids": [],
476 "trace_ids": ["123"],
477 "platform": "myplatform",
478 "release": "myrelease",
479 "dist": "mydist",
480 "environment": "myenv",
481 "tags": [
482 [
483 "tag",
484 "value"
485 ]
486 ]
487}"#;
488
489 let mut replay = Annotated::<Replay>::from_json(json).unwrap();
490 let validation_result = validate(replay.value_mut().as_mut().unwrap());
491 assert!(validation_result.is_err());
492 }
493
494 #[test]
495 fn test_maxchars_trimming() {
496 let json = format!(r#"{{"dist": "{}"}}"#, "0".repeat(100));
497 let mut replay = Annotated::<Replay>::from_json(json.as_str()).unwrap();
498
499 normalize(
500 &mut replay,
501 None,
502 &RawUserAgentInfo::default(),
503 &GeoIpLookup::empty(),
504 );
505 assert_annotated_snapshot!(replay, @r###"
506 {
507 "platform": "other",
508 "dist": "0000000000000000000000000000000000000000000000000000000000000...",
509 "type": "replay_event",
510 "_meta": {
511 "dist": {
512 "": {
513 "rem": [
514 [
515 "!limit",
516 "s",
517 61,
518 64
519 ]
520 ],
521 "len": 100
522 }
523 }
524 }
525 }
526 "###);
527 }
528
529 #[test]
530 fn test_validate_u16_segment_id() {
531 let replay_id =
533 Annotated::new(EventId("52df9022835246eeb317dbd739ccd059".parse().unwrap()));
534 let segment_id: Annotated<u64> = Annotated::new(u16::MAX as u64 + 1);
535 let mut replay = Annotated::new(Replay {
536 replay_id,
537 segment_id,
538 ..Default::default()
539 });
540 assert!(validate(replay.value_mut().as_mut().unwrap()).is_err());
541
542 let replay_id =
544 Annotated::new(EventId("52df9022835246eeb317dbd739ccd059".parse().unwrap()));
545 let segment_id: Annotated<u64> = Annotated::new(u16::MAX as u64);
546 let mut replay = Annotated::new(Replay {
547 replay_id,
548 segment_id,
549 ..Default::default()
550 });
551 assert!(validate(replay.value_mut().as_mut().unwrap()).is_ok());
552 }
553}