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