1use std::borrow::Cow;
2
3use relay_event_schema::processor::{
4 self, Chunk, ProcessValue, ProcessingAction, ProcessingResult, ProcessingState, Processor,
5 ValueType,
6};
7use relay_event_schema::protocol::{Frame, RawStacktrace, Replay};
8use relay_protocol::{Annotated, Array, Empty, Meta, Object, RemarkType, Value};
9
10#[derive(Clone, Debug)]
11struct SizeState {
12 max_depth: Option<usize>,
13 encountered_at_depth: usize,
14 size_remaining: Option<usize>,
15}
16
17#[derive(Default)]
19pub struct TrimmingProcessor {
20 size_state: Vec<SizeState>,
21}
22
23impl TrimmingProcessor {
24 pub fn new() -> Self {
26 Self::default()
27 }
28
29 fn should_remove_container<T: Empty>(&self, value: &T, state: &ProcessingState<'_>) -> bool {
30 self.remaining_depth(state) == Some(1) && !value.is_empty()
33 }
34
35 #[inline]
36 fn remaining_depth(&self, state: &ProcessingState<'_>) -> Option<usize> {
37 self.size_state
38 .iter()
39 .filter_map(|size_state| {
40 let current_depth = state.depth() - size_state.encountered_at_depth;
43 size_state
44 .max_depth
45 .map(|max_depth| max_depth.saturating_sub(current_depth))
46 })
47 .min()
48 }
49
50 #[inline]
51 fn remaining_size(&self) -> Option<usize> {
52 self.size_state
53 .iter()
54 .filter_map(|x| x.size_remaining)
55 .min()
56 }
57}
58
59impl Processor for TrimmingProcessor {
60 fn before_process<T: ProcessValue>(
61 &mut self,
62 _: Option<&T>,
63 _: &mut Meta,
64 state: &ProcessingState<'_>,
65 ) -> ProcessingResult {
66 if state.max_bytes().is_some() || state.attrs().max_depth.is_some() {
70 self.size_state.push(SizeState {
71 size_remaining: state.max_bytes(),
72 encountered_at_depth: state.depth(),
73 max_depth: state.attrs().max_depth,
74 });
75 }
76
77 if state.attrs().trim {
78 if self.remaining_size() == Some(0) {
79 return Err(ProcessingAction::DeleteValueHard);
81 }
82 if self.remaining_depth(state) == Some(0) {
83 return Err(ProcessingAction::DeleteValueHard);
85 }
86 }
87 Ok(())
88 }
89
90 fn after_process<T: ProcessValue>(
91 &mut self,
92 value: Option<&T>,
93 _: &mut Meta,
94 state: &ProcessingState<'_>,
95 ) -> ProcessingResult {
96 self.size_state
99 .pop_if(|size_state| state.depth() == size_state.encountered_at_depth);
100
101 if state.entered_anything() && !self.size_state.is_empty() {
109 let item_length = state
111 .bytes_size()
112 .unwrap_or_else(|| relay_protocol::estimate_size_flat(value) + 1);
113 for size_state in self.size_state.iter_mut() {
114 size_state.size_remaining = size_state
115 .size_remaining
116 .map(|size| size.saturating_sub(item_length));
117 }
118 }
119
120 Ok(())
121 }
122
123 fn process_string(
124 &mut self,
125 value: &mut String,
126 meta: &mut Meta,
127 state: &ProcessingState<'_>,
128 ) -> ProcessingResult {
129 if let Some(max_chars) = state.max_chars() {
130 trim_string(value, meta, max_chars, state.attrs().max_chars_allowance);
131 }
132
133 if !state.attrs().trim {
134 return Ok(());
135 }
136
137 if let Some(size_remaining) = self.remaining_size() {
138 trim_string(value, meta, size_remaining, 0);
139 }
140
141 Ok(())
142 }
143
144 fn process_array<T>(
145 &mut self,
146 value: &mut Array<T>,
147 meta: &mut Meta,
148 state: &ProcessingState<'_>,
149 ) -> ProcessingResult
150 where
151 T: ProcessValue,
152 {
153 if !state.attrs().trim {
154 return Ok(());
155 }
156
157 if !self.size_state.is_empty() {
159 let original_length = value.len();
160
161 if self.should_remove_container(value, state) {
162 return Err(ProcessingAction::DeleteValueHard);
163 }
164
165 let mut split_index = None;
166 for (index, item) in value.iter_mut().enumerate() {
167 if self.remaining_size() == Some(0) {
168 split_index = Some(index);
169 break;
170 }
171
172 let item_state = state.enter_index(index, None, ValueType::for_field(item));
173 processor::process_value(item, self, &item_state)?;
174 }
175
176 if let Some(split_index) = split_index {
177 let _ = value.split_off(split_index);
178 }
179
180 if value.len() != original_length {
181 meta.set_original_length(Some(original_length));
182 }
183 } else {
184 value.process_child_values(self, state)?;
185 }
186
187 Ok(())
188 }
189
190 fn process_object<T>(
191 &mut self,
192 value: &mut Object<T>,
193 meta: &mut Meta,
194 state: &ProcessingState<'_>,
195 ) -> ProcessingResult
196 where
197 T: ProcessValue,
198 {
199 if !state.attrs().trim {
200 return Ok(());
201 }
202
203 if !self.size_state.is_empty() {
205 let original_length = value.len();
206
207 if self.should_remove_container(value, state) {
208 return Err(ProcessingAction::DeleteValueHard);
209 }
210
211 let mut split_key = None;
212 for (key, item) in value.iter_mut() {
213 if self.remaining_size() == Some(0) {
214 split_key = Some(key.to_owned());
215 break;
216 }
217
218 let item_state = state.enter_borrowed(key, None, ValueType::for_field(item));
219 processor::process_value(item, self, &item_state)?;
220 }
221
222 if let Some(split_key) = split_key {
223 let _ = value.split_off(&split_key);
224 }
225
226 if value.len() != original_length {
227 meta.set_original_length(Some(original_length));
228 }
229 } else {
230 value.process_child_values(self, state)?;
231 }
232
233 Ok(())
234 }
235
236 fn process_value(
237 &mut self,
238 value: &mut Value,
239 _meta: &mut Meta,
240 state: &ProcessingState<'_>,
241 ) -> ProcessingResult {
242 if !state.attrs().trim {
243 return Ok(());
244 }
245
246 match value {
247 Value::Array(_) | Value::Object(_) => {
248 if self.remaining_depth(state) == Some(1)
249 && let Ok(x) = serde_json::to_string(&value)
250 {
251 *value = Value::String(x);
253 }
254 }
255 _ => (),
256 }
257
258 value.process_child_values(self, state)?;
259 Ok(())
260 }
261
262 fn process_replay(
263 &mut self,
264 replay: &mut Replay,
265 _: &mut Meta,
266 state: &ProcessingState<'_>,
267 ) -> ProcessingResult {
268 replay.process_child_values(self, state)
269 }
270
271 fn process_raw_stacktrace(
272 &mut self,
273 stacktrace: &mut RawStacktrace,
274 _meta: &mut Meta,
275 state: &ProcessingState<'_>,
276 ) -> ProcessingResult {
277 if !state.attrs().trim {
278 return Ok(());
279 }
280
281 processor::apply(&mut stacktrace.frames, |frames, meta| {
282 enforce_frame_hard_limit(
283 frames,
284 meta,
285 RawStacktrace::MAX_NEWEST_FRAMES,
286 RawStacktrace::MAX_OLDEST_FRAMES,
287 );
288 Ok(())
289 })?;
290
291 stacktrace.process_child_values(self, state)?;
292
293 processor::apply(&mut stacktrace.frames, |frames, _meta| {
294 slim_frame_data(frames, 50);
295 Ok(())
296 })?;
297
298 Ok(())
299 }
300}
301
302pub(crate) fn trim_string(
304 value: &mut String,
305 meta: &mut Meta,
306 max_chars: usize,
307 max_chars_allowance: usize,
308) {
309 let hard_limit = max_chars + max_chars_allowance;
310
311 if bytecount::num_chars(value.as_bytes()) <= hard_limit {
312 return;
313 }
314
315 processor::process_chunked_value(value, meta, |chunks| {
316 let mut length = 0;
317 let mut new_chunks = vec![];
318
319 for chunk in chunks {
320 let chunk_chars = chunk.count();
321
322 if length + chunk_chars < max_chars {
324 new_chunks.push(chunk);
325 length += chunk_chars;
326 continue;
327 }
328
329 match chunk {
330 Chunk::Redaction { .. } => {
333 if length + chunk_chars + 3 < hard_limit {
334 new_chunks.push(chunk);
335 }
336 }
337
338 Chunk::Text { text } => {
340 let mut remaining = String::new();
341 for c in text.chars() {
342 if length + 3 < max_chars {
343 remaining.push(c);
344 } else {
345 break;
346 }
347 length += 1;
348 }
349
350 new_chunks.push(Chunk::Text {
351 text: Cow::Owned(remaining),
352 });
353 }
354 }
355
356 new_chunks.push(Chunk::Redaction {
357 text: Cow::Borrowed("..."),
358 rule_id: Cow::Borrowed("!limit"),
359 ty: RemarkType::Substituted,
360 });
361 break;
362 }
363
364 new_chunks
365 });
366}
367
368fn enforce_frame_hard_limit(
378 frames: &mut Array<Frame>,
379 meta: &mut Meta,
380 recent_frames: usize,
381 old_frames: usize,
382) {
383 let original_length = frames.len();
384 let limit = recent_frames + old_frames;
385 if original_length > limit {
386 meta.set_original_length(Some(original_length));
387 let _ = frames.drain(old_frames..original_length - recent_frames);
388 }
389}
390
391fn slim_frame_data(frames: &mut Array<Frame>, frame_allowance: usize) {
395 let frames_len = frames.len();
396
397 if frames_len <= frame_allowance {
398 return;
399 }
400
401 let mut app_frame_indices = Vec::with_capacity(frames_len);
403 let mut system_frame_indices = Vec::with_capacity(frames_len);
404
405 for (i, frame) in frames.iter().enumerate() {
406 if let Some(frame) = frame.value() {
407 match frame.in_app.value() {
408 Some(true) => app_frame_indices.push(i),
409 _ => system_frame_indices.push(i),
410 }
411 }
412 }
413
414 let app_count = app_frame_indices.len();
415 let system_allowance_half = frame_allowance.saturating_sub(app_count) / 2;
416 let system_frames_to_remove = system_frame_indices
417 .get(system_allowance_half..system_frame_indices.len() - system_allowance_half)
418 .unwrap_or(&[]);
419
420 let remaining = frames_len
421 .saturating_sub(frame_allowance)
422 .saturating_sub(system_frames_to_remove.len());
423 let app_allowance_half = app_count.saturating_sub(remaining) / 2;
424 let app_frames_to_remove = app_frame_indices
425 .get(app_allowance_half..app_frame_indices.len() - app_allowance_half)
426 .unwrap_or(&[]);
427
428 let top_frame_index = frames_len.saturating_sub(1);
431 for i in system_frames_to_remove
432 .iter()
433 .chain(app_frames_to_remove)
434 .filter(|&&i| i != top_frame_index)
435 {
436 if let Some(frame) = frames.get_mut(*i)
437 && let Some(ref mut frame) = frame.value_mut().as_mut()
438 {
439 frame.vars = Annotated::empty();
440 frame.pre_context = Annotated::empty();
441 frame.post_context = Annotated::empty();
442 }
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use std::iter::repeat_n;
449
450 use crate::MaxChars;
451 use chrono::DateTime;
452 use relay_event_schema::protocol::{
453 Breadcrumb, Context, Contexts, Event, Exception, ExtraValue, FlagsContext, PairList,
454 SentryTags, Span, SpanData, SpanId, TagEntry, Tags, Timestamp, TraceId, Values,
455 };
456 use relay_protocol::{FromValue, IntoValue, Map, Remark, SerializableAnnotated, get_value};
457 use similar_asserts::assert_eq;
458
459 use super::*;
460
461 #[test]
462 fn test_string_trimming() {
463 let mut value = Annotated::new("This is my long string I want to have trimmed!".to_owned());
464 processor::apply(&mut value, |v, m| {
465 trim_string(v, m, 20, 0);
466 Ok(())
467 })
468 .unwrap();
469
470 assert_eq!(
471 value,
472 Annotated(Some("This is my long s...".into()), {
473 let mut meta = Meta::default();
474 meta.add_remark(Remark {
475 ty: RemarkType::Substituted,
476 rule_id: "!limit".to_owned(),
477 range: Some((17, 20)),
478 });
479 meta.set_original_length(Some(46));
480 meta
481 })
482 );
483 }
484
485 #[test]
486 fn test_basic_trimming() {
487 let mut processor = TrimmingProcessor::new();
488
489 let mut event = Annotated::new(Event {
490 logger: Annotated::new("x".repeat(300)),
491 ..Default::default()
492 });
493
494 processor::process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
495
496 let mut expected = Annotated::new("x".repeat(300));
497 processor::apply(&mut expected, |v, m| {
498 trim_string(v, m, MaxChars::Logger.limit(), 0);
499 Ok(())
500 })
501 .unwrap();
502
503 assert_eq!(event.value().unwrap().logger, expected);
504 }
505
506 #[test]
507 fn test_max_char_allowance() {
508 let string = "This string requires some allowance to fit!";
509 let mut value = Annotated::new(string.to_owned()); processor::apply(&mut value, |v, m| {
511 trim_string(v, m, 40, 5);
512 Ok(())
513 })
514 .unwrap();
515
516 assert_eq!(value, Annotated::new(string.to_owned()));
517 }
518
519 #[test]
520 fn test_databag_stripping() {
521 let mut processor = TrimmingProcessor::new();
522
523 fn make_nested_object(depth: usize) -> Annotated<Value> {
524 if depth == 0 {
525 return Annotated::new(Value::String("max depth".to_owned()));
526 }
527 let mut rv = Object::new();
528 rv.insert(format!("key{depth}"), make_nested_object(depth - 1));
529 Annotated::new(Value::Object(rv))
530 }
531
532 let databag = Annotated::new({
533 let mut map = Object::new();
534 map.insert(
535 "key_1".to_owned(),
536 Annotated::new(ExtraValue(Value::String("value 1".to_owned()))),
537 );
538 map.insert(
539 "key_2".to_owned(),
540 make_nested_object(8).map_value(ExtraValue),
541 );
542 map.insert(
543 "key_3".to_owned(),
544 make_nested_object(5).map_value(ExtraValue),
546 );
547 map
548 });
549 let mut event = Annotated::new(Event {
550 extra: databag,
551 ..Default::default()
552 });
553
554 processor::process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
555 let stripped_extra = &event.value().unwrap().extra;
556 let json = stripped_extra.to_json_pretty().unwrap();
557
558 assert_eq!(
559 json,
560 r#"{
561 "key_1": "value 1",
562 "key_2": {
563 "key8": {
564 "key7": {
565 "key6": {
566 "key5": {
567 "key4": "{\"key3\":{\"key2\":{\"key1\":\"max depth\"}}}"
568 }
569 }
570 }
571 }
572 },
573 "key_3": {
574 "key5": {
575 "key4": {
576 "key3": {
577 "key2": {
578 "key1": "max depth"
579 }
580 }
581 }
582 }
583 }
584}"#
585 );
586 }
587
588 #[test]
589 fn test_databag_array_stripping() {
590 let mut processor = TrimmingProcessor::new();
591
592 let databag = Annotated::new({
593 let mut map = Object::new();
594 for idx in 0..100 {
595 map.insert(
596 format!("key_{idx}"),
597 Annotated::new(ExtraValue(Value::String("x".repeat(50000)))),
598 );
599 }
600 map
601 });
602 let mut event = Annotated::new(Event {
603 extra: databag,
604 ..Default::default()
605 });
606
607 processor::process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
608 let stripped_extra = SerializableAnnotated(&event.value().unwrap().extra);
609
610 insta::assert_ron_snapshot!(stripped_extra);
611 }
612
613 #[test]
615 fn test_string_trimming_limits() {
616 #[derive(ProcessValue, IntoValue, FromValue, Empty, Debug, Clone)]
617 struct Outer {
618 #[metastructure(max_bytes = 10)]
619 inner: Annotated<Inner>,
620 }
621
622 #[derive(ProcessValue, IntoValue, FromValue, Empty, Debug, Clone)]
623 struct Inner {
624 #[metastructure(max_bytes = 20)]
625 innerer: Annotated<String>,
626 }
627
628 let mut processor = TrimmingProcessor::new();
629
630 let mut outer = Annotated::new({
631 Outer {
632 inner: Annotated::new(Inner {
633 innerer: Annotated::new("This string is 28 bytes long".into()),
634 }),
635 }
636 });
637
638 processor::process_value(&mut outer, &mut processor, ProcessingState::root()).unwrap();
639 let stripped = SerializableAnnotated(&outer);
640
641 insta::assert_ron_snapshot!(stripped, @r###"
642 {
643 "inner": {
644 "innerer": "This st...",
645 },
646 "_meta": {
647 "inner": {
648 "innerer": {
649 "": Meta(Some(MetaInner(
650 rem: [
651 [
652 "!limit",
653 s,
654 7,
655 10,
656 ],
657 ],
658 len: Some(28),
659 ))),
660 },
661 },
662 },
663 }
664 "###);
665 }
666
667 #[test]
668 fn test_tags_stripping() {
669 let mut processor = TrimmingProcessor::new();
670
671 let mut event = Annotated::new(Event {
672 tags: Annotated::new(Tags(
673 vec![Annotated::new(TagEntry(
674 Annotated::new("x".repeat(300)),
675 Annotated::new("x".repeat(300)),
676 ))]
677 .into(),
678 )),
679 ..Default::default()
680 });
681
682 processor::process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
683 let json = event
684 .value()
685 .unwrap()
686 .tags
687 .payload_to_json_pretty()
688 .unwrap();
689
690 assert_eq!(
691 json,
692 r#"[
693 [
694 "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...",
695 "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..."
696 ]
697]"#
698 );
699 }
700
701 #[test]
702 fn test_databag_state_leak() {
703 let event = Annotated::new(Event {
704 breadcrumbs: Annotated::new(Values::new(
705 repeat_n(
706 Annotated::new(Breadcrumb {
707 data: {
708 let mut map = Map::new();
709 map.insert(
710 "spamspamspam".to_owned(),
711 Annotated::new(Value::String("blablabla".to_owned())),
712 );
713 Annotated::new(map)
714 },
715 ..Default::default()
716 }),
717 200,
718 )
719 .collect(),
720 )),
721 exceptions: Annotated::new(Values::new(vec![Annotated::new(Exception {
722 ty: Annotated::new("TypeError".to_owned()),
723 value: Annotated::new("important error message".to_owned().into()),
724 stacktrace: Annotated::new(
725 RawStacktrace {
726 frames: Annotated::new(
727 repeat_n(
728 Annotated::new(Frame {
729 function: Annotated::new("importantFunctionName".to_owned()),
730 symbol: Annotated::new("important_symbol".to_owned()),
731 ..Default::default()
732 }),
733 200,
734 )
735 .collect(),
736 ),
737 ..Default::default()
738 }
739 .into(),
740 ),
741 ..Default::default()
742 })])),
743 ..Default::default()
744 });
745
746 let mut processor = TrimmingProcessor::new();
747 let mut stripped_event = event.clone();
748 processor::process_value(&mut stripped_event, &mut processor, ProcessingState::root())
749 .unwrap();
750
751 assert_eq!(
752 event.to_json_pretty().unwrap(),
753 stripped_event.to_json_pretty().unwrap()
754 );
755 }
756
757 #[test]
758 fn test_custom_context_trimming() {
759 let mut contexts = Contexts::new();
760 for i in 1..2 {
761 contexts.insert(format!("despacito{i}"), {
762 let mut context = Object::new();
763 context.insert(
764 "foo".to_owned(),
765 Annotated::new(Value::String("a".repeat(4000))),
766 );
767 context.insert(
768 "bar".to_owned(),
769 Annotated::new(Value::String("a".repeat(5000))),
770 );
771 Context::Other(context)
772 });
773 }
774
775 let mut contexts = Annotated::new(contexts);
776 let mut processor = TrimmingProcessor::new();
777 processor::process_value(&mut contexts, &mut processor, ProcessingState::root()).unwrap();
778
779 let contexts = contexts.value().unwrap();
780 for i in 1..2 {
781 let other = match contexts.get_key(format!("despacito{i}")).unwrap() {
782 Context::Other(x) => x,
783 _ => panic!("Context has changed type!"),
784 };
785
786 assert_eq!(
787 other
788 .get("bar")
789 .unwrap()
790 .value()
791 .unwrap()
792 .as_str()
793 .unwrap()
794 .len(),
795 5000
796 );
797 assert_eq!(
798 other
799 .get("foo")
800 .unwrap()
801 .value()
802 .unwrap()
803 .as_str()
804 .unwrap()
805 .len(),
806 3189
807 );
808 }
809 }
810
811 #[test]
812 fn test_extra_trimming_long_arrays() {
813 let mut extra = Object::new();
814 extra.insert("foo".to_owned(), {
815 Annotated::new(ExtraValue(Value::Array(
816 repeat_n(Annotated::new(Value::U64(1)), 200_000).collect(),
817 )))
818 });
819
820 let mut event = Annotated::new(Event {
821 extra: Annotated::new(extra),
822 ..Default::default()
823 });
824
825 let mut processor = TrimmingProcessor::new();
826 processor::process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
827
828 let arr = match event
829 .value()
830 .unwrap()
831 .extra
832 .value()
833 .unwrap()
834 .get("foo")
835 .unwrap()
836 .value()
837 .unwrap()
838 {
839 ExtraValue(Value::Array(x)) => x,
840 x => panic!("Wrong type: {x:?}"),
841 };
842
843 assert_eq!(arr.len(), 8192);
845 }
846
847 #[test]
890 fn test_frameqty_equals_limit() {
891 fn create_frame(filename: &str) -> Annotated<Frame> {
892 Annotated::new(Frame {
893 filename: Annotated::new(filename.into()),
894 ..Default::default()
895 })
896 }
897
898 let mut frames = Annotated::new(vec![
899 create_frame("foo3.py"),
900 create_frame("foo4.py"),
901 create_frame("foo5.py"),
902 ]);
903
904 processor::apply(&mut frames, |f, m| {
905 enforce_frame_hard_limit(f, m, 3, 0);
906 Ok(())
907 })
908 .unwrap();
909
910 processor::apply(&mut frames, |f, m| {
911 enforce_frame_hard_limit(f, m, 1, 2);
912 Ok(())
913 })
914 .unwrap();
915
916 assert!(frames.meta().original_length().is_none());
918 }
919
920 #[test]
921 fn test_frame_hard_limit() {
922 fn create_frame(filename: &str) -> Annotated<Frame> {
923 Annotated::new(Frame {
924 filename: Annotated::new(filename.into()),
925 ..Default::default()
926 })
927 }
928
929 let mut frames = Annotated::new(vec![
930 create_frame("foo1.py"),
931 create_frame("foo2.py"),
932 create_frame("foo3.py"),
933 create_frame("foo4.py"),
934 create_frame("foo5.py"),
935 ]);
936
937 processor::apply(&mut frames, |f, m| {
938 enforce_frame_hard_limit(f, m, 3, 0);
939 Ok(())
940 })
941 .unwrap();
942
943 let mut expected_meta = Meta::default();
944 expected_meta.set_original_length(Some(5));
945
946 assert_eq!(
947 frames,
948 Annotated(
949 Some(vec![
950 create_frame("foo3.py"),
951 create_frame("foo4.py"),
952 create_frame("foo5.py"),
953 ]),
954 expected_meta
955 )
956 );
957 }
958
959 #[test]
960 fn test_frame_hard_limit_recent_old() {
961 fn create_frame(filename: &str) -> Annotated<Frame> {
962 Annotated::new(Frame {
963 filename: Annotated::new(filename.into()),
964 ..Default::default()
965 })
966 }
967
968 let mut frames = Annotated::new(vec![
969 create_frame("foo1.py"),
970 create_frame("foo2.py"),
971 create_frame("foo3.py"),
972 create_frame("foo4.py"),
973 create_frame("foo5.py"),
974 ]);
975
976 processor::apply(&mut frames, |f, m| {
977 enforce_frame_hard_limit(f, m, 2, 1);
978 Ok(())
979 })
980 .unwrap();
981
982 let mut expected_meta = Meta::default();
983 expected_meta.set_original_length(Some(5));
984
985 assert_eq!(
986 frames,
987 Annotated(
988 Some(vec![
989 create_frame("foo1.py"),
990 create_frame("foo4.py"),
991 create_frame("foo5.py"),
992 ]),
993 expected_meta
994 )
995 );
996 }
997
998 #[test]
999 fn test_slim_frame_data_under_max() {
1000 let mut frames = vec![Annotated::new(Frame {
1001 filename: Annotated::new("foo".into()),
1002 pre_context: Annotated::new(vec![Annotated::new("a".to_owned())]),
1003 context_line: Annotated::new("b".to_owned()),
1004 post_context: Annotated::new(vec![Annotated::new("c".to_owned())]),
1005 ..Default::default()
1006 })];
1007
1008 let old_frames = frames.clone();
1009 slim_frame_data(&mut frames, 4);
1010
1011 assert_eq!(frames, old_frames);
1012 }
1013
1014 #[test]
1015 fn test_slim_frame_data_over_max() {
1016 let mut frames = vec![];
1017
1018 for n in 0..5 {
1019 frames.push(Annotated::new(Frame {
1020 filename: Annotated::new(format!("foo {n}").into()),
1021 pre_context: Annotated::new(vec![Annotated::new("a".to_owned())]),
1022 context_line: Annotated::new("b".to_owned()),
1023 post_context: Annotated::new(vec![Annotated::new("c".to_owned())]),
1024 ..Default::default()
1025 }));
1026 }
1027
1028 slim_frame_data(&mut frames, 4);
1029
1030 let expected = vec![
1031 Annotated::new(Frame {
1032 filename: Annotated::new("foo 0".into()),
1033 pre_context: Annotated::new(vec![Annotated::new("a".to_owned())]),
1034 context_line: Annotated::new("b".to_owned()),
1035 post_context: Annotated::new(vec![Annotated::new("c".to_owned())]),
1036 ..Default::default()
1037 }),
1038 Annotated::new(Frame {
1039 filename: Annotated::new("foo 1".into()),
1040 pre_context: Annotated::new(vec![Annotated::new("a".to_owned())]),
1041 context_line: Annotated::new("b".to_owned()),
1042 post_context: Annotated::new(vec![Annotated::new("c".to_owned())]),
1043 ..Default::default()
1044 }),
1045 Annotated::new(Frame {
1046 filename: Annotated::new("foo 2".into()),
1047 context_line: Annotated::new("b".to_owned()),
1048 ..Default::default()
1049 }),
1050 Annotated::new(Frame {
1051 filename: Annotated::new("foo 3".into()),
1052 pre_context: Annotated::new(vec![Annotated::new("a".to_owned())]),
1053 context_line: Annotated::new("b".to_owned()),
1054 post_context: Annotated::new(vec![Annotated::new("c".to_owned())]),
1055 ..Default::default()
1056 }),
1057 Annotated::new(Frame {
1058 filename: Annotated::new("foo 4".into()),
1059 pre_context: Annotated::new(vec![Annotated::new("a".to_owned())]),
1060 context_line: Annotated::new("b".to_owned()),
1061 post_context: Annotated::new(vec![Annotated::new("c".to_owned())]),
1062 ..Default::default()
1063 }),
1064 ];
1065
1066 assert_eq!(frames, expected);
1067 }
1068
1069 #[test]
1070 fn test_slim_frame_data_does_not_trim_top_frame_metadata() {
1071 let mut frames: Array<Frame> = (0..50)
1072 .map(|n| {
1073 Annotated::new(Frame {
1074 filename: Annotated::new(format!("system {n}").into()),
1075 ..Default::default()
1076 })
1077 })
1078 .collect();
1079 frames.push(Annotated::new(Frame {
1080 filename: Annotated::new("raising".into()),
1081 pre_context: Annotated::new(vec![Annotated::new("before".to_owned())]),
1082 context_line: Annotated::new("current".to_owned()),
1083 post_context: Annotated::new(vec![Annotated::new("after".to_owned())]),
1084 vars: Annotated::new({
1085 let mut vars = Object::new();
1086 vars.insert("local".to_owned(), Annotated::new("value".into()));
1087 vars.into()
1088 }),
1089 in_app: Annotated::new(true),
1090 ..Default::default()
1091 }));
1092
1093 slim_frame_data(&mut frames, 50);
1094
1095 let top_frame = frames.last().unwrap().value().unwrap();
1096 assert!(top_frame.vars.value().is_some());
1097 assert!(top_frame.pre_context.value().is_some());
1098 assert!(top_frame.context_line.value().is_some());
1099 assert!(top_frame.post_context.value().is_some());
1100 }
1101
1102 #[test]
1103 fn test_too_many_spans_trimmed() {
1104 let span = Span {
1105 platform: Annotated::new("a".repeat(1024 * 90)),
1106 sentry_tags: Annotated::new(SentryTags {
1107 release: Annotated::new("b".repeat(1024 * 100)),
1108 ..Default::default()
1109 }),
1110 ..Default::default()
1111 };
1112 let spans: Vec<_> = std::iter::repeat_with(|| Annotated::new(span.clone()))
1113 .take(10)
1114 .collect();
1115
1116 let mut event = Annotated::new(Event {
1117 spans: Annotated::new(spans.clone()),
1118 ..Default::default()
1119 });
1120
1121 let mut processor = TrimmingProcessor::new();
1122 processor::process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1123
1124 let trimmed_spans = event.0.unwrap().spans.0.unwrap();
1125 assert_eq!(trimmed_spans.len(), 5);
1126
1127 assert_eq!(trimmed_spans.as_slice(), &spans[0..5]);
1129 }
1130
1131 #[test]
1132 fn test_span_data_not_partially_trimmed() {
1133 let span_data = SpanData::from([(
1134 "large_attribute".to_owned(),
1135 Annotated::new(Value::String("a".repeat(100 * 1024))),
1136 )]);
1137 let span = Span {
1138 data: Annotated::new(span_data.clone()),
1139 ..Default::default()
1140 };
1141 let spans: Vec<_> = std::iter::repeat_with(|| Annotated::new(span.clone()))
1142 .take(10)
1143 .collect();
1144
1145 let mut event = Annotated::new(Event {
1146 spans: Annotated::new(spans.clone()),
1147 ..Default::default()
1148 });
1149
1150 let mut processor = TrimmingProcessor::new();
1151 processor::process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1152
1153 let trimmed = event.value().unwrap().spans.value().unwrap();
1154 assert!(trimmed.len() < spans.len());
1155 assert!(trimmed.iter().all(|span| {
1156 span.value()
1157 .and_then(|span| span.data.value())
1158 .is_some_and(|data| data == &span_data)
1159 }));
1160 }
1161
1162 #[test]
1163 fn test_untrimmable_fields() {
1164 let original_description = "a".repeat(819163);
1165 let original_trace_id: TraceId = "b".repeat(32).parse().unwrap();
1166 let mut event = Annotated::new(Event {
1167 spans: Annotated::new(vec![
1168 Span {
1169 description: original_description.clone().into(),
1170 ..Default::default()
1171 }
1172 .into(),
1173 Span {
1174 trace_id: original_trace_id.into(),
1175 ..Default::default()
1176 }
1177 .into(),
1178 ]),
1179 ..Default::default()
1180 });
1181
1182 let mut processor = TrimmingProcessor::new();
1183 processor::process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1184
1185 assert_eq!(
1186 get_value!(event.spans[0].description!),
1187 &original_description
1188 );
1189 assert_eq!(get_value!(event.spans[1].trace_id!), &original_trace_id);
1191 }
1192
1193 #[test]
1194 fn test_untrimmable_fields_drop() {
1195 let original_description = "a".repeat(819164);
1196 let original_span_id: SpanId = "b".repeat(16).parse().unwrap();
1197 let original_trace_id: TraceId = "c".repeat(32).parse().unwrap();
1198 let original_segment_id: SpanId = "d".repeat(16).parse().unwrap();
1199 let original_op = "e".repeat(129);
1200
1201 let mut event = Annotated::new(Event {
1202 spans: Annotated::new(vec![
1203 Span {
1204 description: original_description.clone().into(),
1205 ..Default::default()
1206 }
1207 .into(),
1208 Span {
1209 span_id: original_span_id.into(),
1210 trace_id: original_trace_id.into(),
1211 segment_id: original_segment_id.into(),
1212 is_segment: false.into(),
1213 op: original_op.clone().into(),
1214 start_timestamp: Timestamp(
1215 DateTime::parse_from_rfc3339("1996-12-19T16:39:57Z")
1216 .unwrap()
1217 .into(),
1218 )
1219 .into(),
1220 timestamp: Timestamp(
1221 DateTime::parse_from_rfc3339("1996-12-19T16:39:58Z")
1222 .unwrap()
1223 .into(),
1224 )
1225 .into(),
1226 ..Default::default()
1227 }
1228 .into(),
1229 ]),
1230 ..Default::default()
1231 });
1232
1233 let mut processor = TrimmingProcessor::new();
1234 processor::process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1235
1236 assert_eq!(
1237 get_value!(event.spans[0].description!),
1238 &original_description
1239 );
1240 assert_eq!(get_value!(event.spans[1].span_id!), &original_span_id);
1242 assert_eq!(get_value!(event.spans[1].trace_id!), &original_trace_id);
1243 assert_eq!(get_value!(event.spans[1].segment_id!), &original_segment_id);
1244 assert_eq!(get_value!(event.spans[1].is_segment!), &false);
1245 assert_eq!(get_value!(event.spans[1].op!).len(), 128);
1247 assert!(get_value!(event.spans[1].start_timestamp).is_some());
1248 assert!(get_value!(event.spans[1].timestamp).is_some());
1249 }
1250
1251 #[test]
1252 fn test_too_long_tags() {
1253 let mut event = Annotated::new(Event {
1254 tags: Annotated::new(Tags(PairList(
1255 vec![Annotated::new(TagEntry(
1256 Annotated::new("foobar".to_owned()),
1257 Annotated::new("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".to_owned()),
1258 )), Annotated::new(TagEntry(
1259 Annotated::new("foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo".to_owned()),
1260 Annotated::new("bar".to_owned()),
1261 ))]),
1262 )),
1263 ..Event::default()
1264 });
1265
1266 let mut processor = TrimmingProcessor::new();
1267 processor::process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1268
1269 insta::assert_debug_snapshot!(get_value!(event.tags!), @r###"
1270 Tags(
1271 PairList(
1272 [
1273 TagEntry(
1274 "foobar",
1275 Annotated(
1276 "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...",
1277 Meta {
1278 remarks: [
1279 Remark {
1280 ty: Substituted,
1281 rule_id: "!limit",
1282 range: Some(
1283 (
1284 197,
1285 200,
1286 ),
1287 ),
1288 },
1289 ],
1290 errors: [],
1291 original_length: Some(
1292 203,
1293 ),
1294 original_value: None,
1295 },
1296 ),
1297 ),
1298 TagEntry(
1299 Annotated(
1300 "foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo...",
1301 Meta {
1302 remarks: [
1303 Remark {
1304 ty: Substituted,
1305 rule_id: "!limit",
1306 range: Some(
1307 (
1308 197,
1309 200,
1310 ),
1311 ),
1312 },
1313 ],
1314 errors: [],
1315 original_length: Some(
1316 203,
1317 ),
1318 original_value: None,
1319 },
1320 ),
1321 "bar",
1322 ),
1323 ],
1324 ),
1325 )
1326 "###);
1327 }
1328
1329 #[test]
1330 fn test_fixed_item_size() {
1331 #[derive(Debug, Clone, Empty, IntoValue, FromValue, ProcessValue)]
1332 struct TestObject {
1333 #[metastructure(max_bytes = 28)]
1334 inner: Annotated<TestObjectInner>,
1335 }
1336 #[derive(Debug, Clone, Empty, IntoValue, FromValue, ProcessValue)]
1337 struct TestObjectInner {
1338 #[metastructure(max_chars = 10, trim = true)]
1339 body: Annotated<String>,
1340 #[metastructure(trim = false, bytes_size = "always_zero")]
1342 number: Annotated<u64>,
1343 #[metastructure(trim = false, bytes_size = 10)]
1345 other_number: Annotated<u64>,
1346 #[metastructure(trim = true)]
1347 footer: Annotated<String>,
1348 }
1349
1350 fn always_zero(_state: &ProcessingState) -> Option<usize> {
1351 Some(0)
1352 }
1353
1354 let mut object = Annotated::new(TestObject {
1355 inner: Annotated::new(TestObjectInner {
1356 body: Annotated::new("Longer than 10 chars".to_owned()),
1357 number: Annotated::new(13),
1358 other_number: Annotated::new(12),
1359 footer: Annotated::new("There should only be 'Th...' left".to_owned()),
1360 }),
1361 });
1362
1363 let mut processor = TrimmingProcessor::new();
1364 processor::process_value(&mut object, &mut processor, ProcessingState::root()).unwrap();
1365
1366 insta::assert_ron_snapshot!(SerializableAnnotated(&object), @r###"
1371 {
1372 "inner": {
1373 "body": "Longer ...",
1374 "number": 13,
1375 "other_number": 12,
1376 "footer": "Th...",
1377 },
1378 "_meta": {
1379 "inner": {
1380 "body": {
1381 "": Meta(Some(MetaInner(
1382 rem: [
1383 [
1384 "!limit",
1385 s,
1386 7,
1387 10,
1388 ],
1389 ],
1390 len: Some(20),
1391 ))),
1392 },
1393 "footer": {
1394 "": Meta(Some(MetaInner(
1395 rem: [
1396 [
1397 "!limit",
1398 s,
1399 2,
1400 5,
1401 ],
1402 ],
1403 len: Some(33),
1404 ))),
1405 },
1406 },
1407 },
1408 }
1409 "###);
1410 }
1411
1412 #[test]
1413 fn test_flags_context_trimming() {
1414 let original_flags_count = 1_000;
1415 let values: Vec<_> = (0..original_flags_count)
1416 .map(|i| {
1417 serde_json::json!({
1418 "flag": format!("feature.flag.{i}"),
1419 "result": "x".repeat(500),
1420 })
1421 })
1422 .collect();
1423 let json = serde_json::json!({
1424 "contexts": {
1425 "flags": {
1426 "values": values,
1427 },
1428 "my_custom_context": {
1429 "foo": "x".repeat(10_000)
1430 }
1431 },
1432 })
1433 .to_string();
1434 let mut event = Annotated::<Event>::from_json(&json).unwrap();
1435
1436 let mut processor = TrimmingProcessor::new();
1437 processor::process_value(&mut event, &mut processor, ProcessingState::root()).unwrap();
1438
1439 let contexts = get_value!(event.contexts!);
1440
1441 let values = &contexts.get::<FlagsContext>().unwrap().values;
1443
1444 assert_eq!(values.value().unwrap().len(), 584);
1445 assert_eq!(values.meta().original_length(), Some(original_flags_count));
1446
1447 let custom = match contexts.get_key("my_custom_context").unwrap() {
1449 Context::Other(custom) => custom,
1450 _ => unreachable!(),
1451 };
1452 assert_eq!(custom["foo"].value().unwrap().as_str().unwrap().len(), 8192);
1453 assert_eq!(custom["foo"].meta().original_length(), Some(10_000));
1454 }
1455}