From 21baee1e46a7d2b4c2cbcf6f09331038fca6d0ae Mon Sep 17 00:00:00 2001 From: Catalin Irimie Date: Fri, 24 Jul 2026 15:55:59 +0300 Subject: [PATCH 1/3] feat(cymbal): set posthog-python wire-order cutoff at 7.30.0 --- .../src/modes/processing/normalization.rs | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/rust/cymbal/src/modes/processing/normalization.rs b/rust/cymbal/src/modes/processing/normalization.rs index c79374c0828f..145caf97b919 100644 --- a/rust/cymbal/src/modes/processing/normalization.rs +++ b/rust/cymbal/src/modes/processing/normalization.rs @@ -124,7 +124,8 @@ fn lib_rules() -> &'static [LibRule] { rules.push(LibRule { lib: "posthog-python", fix: WireOrderFix::EXCEPTION_LIST, - canonical_since: None, + // posthog-python ships the flip in 7.30.0 (PostHog/posthog-python#728). + canonical_since: Some(Version::new(7, 30, 0)), }); rules @@ -331,6 +332,45 @@ mod test { assert!(legacy_wire_order(None, &canonical).is_none()); } + #[test] + fn python_cutoff_gates_exception_list_normalization_by_version() { + // Python's fix reverses the exception list (root-cause-first -> + // outermost-first), not frames — the gate must key on the same fix. + let make_list = || -> ExceptionList { + vec![ + exception_with_frames("RootCause", &["main", "boom"]), + exception_with_frames("Wrapper", &["main", "wrap"]), + ] + .into() + }; + + for version in [Some("7.29.0"), Some("6.0.0"), Some("garbage"), None] { + let mut list = make_list(); + let legacy = normalize_wire_order(&mut list, Some("posthog-python"), version); + assert!(legacy.is_some(), "{version:?} should normalize"); + assert_eq!(list[0].exception_type, "Wrapper", "list reversed"); + assert_eq!( + frame_names(&list[0]), + vec!["main", "wrap"], + "frames untouched" + ); + } + + for version in ["7.30.0", "7.30.1", "7.31.0", "8.0.0"] { + let mut list = make_list(); + let legacy = normalize_wire_order(&mut list, Some("posthog-python"), Some(version)); + assert!(legacy.is_none(), "{version} should pass through"); + assert_eq!(list[0].exception_type, "RootCause", "list untouched"); + } + + // Legacy hashing still reconstructs pre-flip list order post-cutoff. + let canonical = make_list(); + let legacy = legacy_wire_order(Some("posthog-python"), &canonical) + .expect("reconstruction must ignore the cutoff"); + assert_eq!(legacy[0].exception_type, "Wrapper"); + assert_eq!(frame_names(&legacy[0]), vec!["main", "wrap"]); + } + #[test] fn elixir_cutoff_gates_normalization_by_version() { for version in [Some("2.12.1"), Some("2.1.0"), Some("garbage"), None] { From 09a31e4dcfa921bead98d46c269f18e755feaf53 Mon Sep 17 00:00:00 2001 From: Catalin Irimie Date: Mon, 27 Jul 2026 20:01:45 +0300 Subject: [PATCH 2/3] fix(cymbal): move posthog-python cutoff to 7.31.0 7.30.0 was claimed by an unrelated release before the flip merged. --- rust/cymbal/src/modes/processing/normalization.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rust/cymbal/src/modes/processing/normalization.rs b/rust/cymbal/src/modes/processing/normalization.rs index 145caf97b919..455095075cb3 100644 --- a/rust/cymbal/src/modes/processing/normalization.rs +++ b/rust/cymbal/src/modes/processing/normalization.rs @@ -124,8 +124,8 @@ fn lib_rules() -> &'static [LibRule] { rules.push(LibRule { lib: "posthog-python", fix: WireOrderFix::EXCEPTION_LIST, - // posthog-python ships the flip in 7.30.0 (PostHog/posthog-python#728). - canonical_since: Some(Version::new(7, 30, 0)), + // posthog-python ships the flip in 7.31.0 (PostHog/posthog-python#728). + canonical_since: Some(Version::new(7, 31, 0)), }); rules @@ -344,7 +344,7 @@ mod test { .into() }; - for version in [Some("7.29.0"), Some("6.0.0"), Some("garbage"), None] { + for version in [Some("7.30.1"), Some("7.30.0"), Some("garbage"), None] { let mut list = make_list(); let legacy = normalize_wire_order(&mut list, Some("posthog-python"), version); assert!(legacy.is_some(), "{version:?} should normalize"); @@ -356,7 +356,7 @@ mod test { ); } - for version in ["7.30.0", "7.30.1", "7.31.0", "8.0.0"] { + for version in ["7.31.0", "7.31.1", "7.32.0", "8.0.0"] { let mut list = make_list(); let legacy = normalize_wire_order(&mut list, Some("posthog-python"), Some(version)); assert!(legacy.is_none(), "{version} should pass through"); From c1bd47b4c5abbe5d76d6d0eda98cf3612f511d08 Mon Sep 17 00:00:00 2001 From: Catalin Irimie Date: Mon, 27 Jul 2026 20:38:26 +0300 Subject: [PATCH 3/3] test(cymbal): use canonical fixture orientation for python post-cutoff cases --- .../src/modes/processing/normalization.rs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/rust/cymbal/src/modes/processing/normalization.rs b/rust/cymbal/src/modes/processing/normalization.rs index 455095075cb3..f976d717cc76 100644 --- a/rust/cymbal/src/modes/processing/normalization.rs +++ b/rust/cymbal/src/modes/processing/normalization.rs @@ -356,19 +356,32 @@ mod test { ); } + // Post-flip senders emit canonical order: outermost wrapper first. + let make_canonical_list = || -> ExceptionList { + vec![ + exception_with_frames("Wrapper", &["main", "wrap"]), + exception_with_frames("RootCause", &["main", "boom"]), + ] + .into() + }; + for version in ["7.31.0", "7.31.1", "7.32.0", "8.0.0"] { - let mut list = make_list(); + let mut list = make_canonical_list(); let legacy = normalize_wire_order(&mut list, Some("posthog-python"), Some(version)); assert!(legacy.is_none(), "{version} should pass through"); - assert_eq!(list[0].exception_type, "RootCause", "list untouched"); + assert_eq!( + list[0].exception_type, "Wrapper", + "canonical list untouched" + ); } - // Legacy hashing still reconstructs pre-flip list order post-cutoff. - let canonical = make_list(); + // Legacy hashing still reconstructs the pre-flip (root-cause-first) + // list order from the canonical stored order post-cutoff. + let canonical = make_canonical_list(); let legacy = legacy_wire_order(Some("posthog-python"), &canonical) .expect("reconstruction must ignore the cutoff"); - assert_eq!(legacy[0].exception_type, "Wrapper"); - assert_eq!(frame_names(&legacy[0]), vec!["main", "wrap"]); + assert_eq!(legacy[0].exception_type, "RootCause"); + assert_eq!(frame_names(&legacy[0]), vec!["main", "boom"]); } #[test]