diff --git a/frontend/__snapshots__/scenes-app-batchexports--exports--dark.png b/frontend/__snapshots__/scenes-app-batchexports--exports--dark.png index c26bfcde41da7..8b08d084fcd3b 100644 Binary files a/frontend/__snapshots__/scenes-app-batchexports--exports--dark.png and b/frontend/__snapshots__/scenes-app-batchexports--exports--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-batchexports--exports--light.png b/frontend/__snapshots__/scenes-app-batchexports--exports--light.png index c9f83584af268..2cb1ba1077f09 100644 Binary files a/frontend/__snapshots__/scenes-app-batchexports--exports--light.png and b/frontend/__snapshots__/scenes-app-batchexports--exports--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--dark--webkit.png index bc6d2e800b233..d6b7eb401f2d2 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--dark.png index 575234ed7ef07..344f72c3deef4 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--light--webkit.png index 455a435c50b31..324355d0243e7 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--light.png index daa7e7d3e7dc0..7b67b19f852de 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark--webkit.png index 88c2b14c8b0b9..8f746a8691900 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark.png index f759b15b33464..073fd1cfd9b4d 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light--webkit.png index 3a69b9e83707d..d533e0d8f9f33 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light.png index 663607b53e8ea..291f8b46aeaa7 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark--webkit.png index 716ea6837904d..a3ee2db3746af 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark.png index 6bc9e246d68f8..94a5ee02ec4d8 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--light--webkit.png index 5f1f1d92437b1..b34882ace11cc 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--light.png index d2181932c7f05..4edbfeac60e82 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--dark--webkit.png index 2cc3b8ce22b5b..de7854b8a268c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--dark.png index e56c597585fe1..450f4df2e5086 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--light--webkit.png index 4038bd965b877..4f3aaff555e73 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--light.png index 854baf356aebf..2caada3e5f0aa 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark--webkit.png index 902a8106b2ecf..5a8dd279c4098 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark.png index 129f8a7c664eb..2e7597327dd95 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light--webkit.png index 535a804029e3b..f07390be2bde5 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light.png index ce6b88c8b832a..43b1e9d375df0 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--dark--webkit.png index 1d78dabd80855..e4f2f16fb20cb 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--dark.png index 44cc3fcc624c7..5ef12b9284334 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light--webkit.png index 75a57df723c0c..63239ee7e9c1b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light.png index ba38c8ca035a7..bc480012967fe 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--dark--webkit.png index 21553a50b6531..c287ce9981c4b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--dark.png index d057c280e852a..ab96b0d690b72 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--light--webkit.png index 513177e17b02e..095df26be5723 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--light.png b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--light.png index e340ca1e2fec8..9fac34d754133 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--dark--webkit.png index 879dbd09a75ce..c950df2fccc84 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--dark.png index ac0f6986bff7d..85bd4ba1299cd 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--light--webkit.png index 0212d628ef242..89166c108bf90 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--light.png index 6af782a80368f..c8ae397ec27b7 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--retention-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--retention-edit--dark--webkit.png index 33375878685ff..c68cbd4afaa7b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--retention-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--retention-edit--dark.png index c141849f38c7b..150fd74f16754 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--retention-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--retention-edit--light--webkit.png index c560bb36af1df..ad6cef32c6170 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--retention-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--retention-edit--light.png b/frontend/__snapshots__/scenes-app-insights--retention-edit--light.png index 2af238075ff92..8085a27d8023e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--retention-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--retention-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--dark--webkit.png index 7f4281a6737e4..b3c2aac2663cb 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--dark.png index 1b7682973150f..500aabe4c16dd 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--light--webkit.png index 6b5fa648a1d4b..2b971daa6d8aa 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--light.png b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--light.png index 5fedaa853ecac..f9e2e405fe7ab 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--stickiness-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--stickiness-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark--webkit.png index 9d1ff6c58a2e4..f15a52e0adeb7 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark.png index 42935a08f7b53..c63bec3bce157 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light--webkit.png index 4d3ced623edbe..efc991b09979b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light.png index 942b7487b4190..bd7561034d2b6 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark--webkit.png index 24e3a2afed4b3..7eda5ccb5ec8f 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark.png index 2cb7a8fb914d0..031416c77e653 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light--webkit.png index fa9f84a4ba714..cb3392ec04d8c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light.png index dabf1687084f3..96ee57257d5db 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark--webkit.png index c6f88a5b288f2..f6c83a9e6c855 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark.png index 78323ed226515..1d7e811296376 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light--webkit.png index 63b6264d0ddcb..bd0e45b8b0482 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light.png index 497451f111003..103c6119e6fb2 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark--webkit.png index 72afaae55a6de..1fb56f38f495c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark.png index 7696eacfcae73..e4e23a2d8c764 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light--webkit.png index 057845cb3d383..03ef62763c14f 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light.png index 72e4f2a949c45..7316fb7aa2f1e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark--webkit.png index fbf8ab4b5a97f..1523b84cc86e1 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark.png index 8fb3b78df8373..256d49c9cec1a 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light--webkit.png index e3e4c32f81aa6..780c2c6bb3cbc 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light.png index f97959a7caec2..ef943248640ba 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark--webkit.png index 1ecfd71f121ad..a24f6138139ca 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark.png index 602535c876235..ba7351d0470e8 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light--webkit.png index 829a8dbcb2414..0663ea14afe27 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light.png index ec763aa79982a..74ee2a99f91ef 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark--webkit.png index 58c16c493ecd2..c488cb571ccc9 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark.png index 183894fab5689..93dbea178b4cf 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light--webkit.png index 5335ee79ea2c5..b8a21d2a9f0ab 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light.png index 5490be89c2d59..964c47e56fe9c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--dark--webkit.png index 339c1436bea19..976663635232c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--dark.png index ba4b8e99f82fe..67b0af12b8809 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--light--webkit.png index 3990924ee4605..42b5f935da567 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--light.png index 87039d7c7e288..c7176cd1facc8 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--dark--webkit.png index 51b2f45767cb7..bd1f122c8871b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--dark.png index fb80bc1e47f74..b1a9e60f7d5bd 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--light--webkit.png index 52668967146b3..2282f2e9ba1ed 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--light.png index bd333c9141210..c1b8261c6a5ee 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-value-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-value-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark--webkit.png index 58c196a6b6a65..2cc6079eb8e50 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark.png index 6bc096cdc5906..348137ecb0667 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light--webkit.png index 51f2149df17f4..6455ad387b355 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light.png index a54c0c7d17c72..5633ed07bf6ec 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--dark--webkit.png index 85ce2e0d9b33c..2f8a18cd1672a 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--dark.png index 771cc514bfd3a..104a4d9515fe1 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light--webkit.png index a1b1f2e19445e..3bfa65b4ca644 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light.png b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light.png index 8945a55c6ae91..db3a5409f56f8 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light.png differ diff --git a/frontend/src/queries/nodes/InsightQuery/utils/cleanProperties.test.ts b/frontend/src/queries/nodes/InsightQuery/utils/cleanProperties.test.ts new file mode 100644 index 0000000000000..2e39095876397 --- /dev/null +++ b/frontend/src/queries/nodes/InsightQuery/utils/cleanProperties.test.ts @@ -0,0 +1,135 @@ +import { cleanEntityProperties, cleanGlobalProperties } from './cleanProperties' + +describe('cleanGlobalProperties', () => { + it('handles empty properties', () => { + const properties = {} + + const result = cleanGlobalProperties(properties) + + expect(result).toEqual(undefined) + }) + + it('handles old style properties', () => { + const properties = { utm_medium__icontains: 'email' } + + const result = cleanGlobalProperties(properties) + + expect(result).toEqual({ + type: 'AND', + values: [ + { + type: 'AND', + values: [ + { + key: 'utm_medium', + operator: 'icontains', + type: 'event', + value: 'email', + }, + ], + }, + ], + }) + }) + + it('handles property filter lists', () => { + const properties = [{ key: 'id', type: 'cohort', value: 636, operator: null }] + + const result = cleanGlobalProperties(properties) + + expect(result).toEqual({ + type: 'AND', + values: [{ type: 'AND', values: [{ key: 'id', type: 'cohort', value: 636 }] }], + }) + }) + + it('handles property group filters', () => { + const properties = { + type: 'AND', + values: [{ type: 'AND', values: [{ key: 'id', type: 'cohort', value: 850, operator: null }] }], + } + + const result = cleanGlobalProperties(properties) + + expect(result).toEqual(properties) + }) + + it('handles property group filters values', () => { + const properties = { + type: 'AND', + values: [{ key: 'id', type: 'cohort', value: 850, operator: null }], + } + + const result = cleanGlobalProperties(properties) + + expect(result).toEqual({ + type: 'AND', + values: [ + { + type: 'AND', + values: [{ key: 'id', type: 'cohort', value: 850 }], + }, + ], + }) + }) +}) + +describe('cleanEntityProperties', () => { + it('handles empty properties', () => { + const properties = {} + + const result = cleanEntityProperties(properties) + + expect(result).toEqual(undefined) + }) + + it('handles old style properties', () => { + const properties = { utm_medium__icontains: 'email' } + + const result = cleanEntityProperties(properties) + + expect(result).toEqual([ + { + key: 'utm_medium', + operator: 'icontains', + type: 'event', + value: 'email', + }, + ]) + }) + + it('handles property filter lists', () => { + const properties = [ + { key: '$current_url', type: 'event', value: 'https://hedgebox.net/signup/', operator: 'exact' }, + ] + + const result = cleanEntityProperties(properties) + + expect(result).toEqual(properties) + }) + + it('handles property group values', () => { + const properties = { + type: 'AND', + values: [ + { + key: '$current_url', + operator: 'exact', + type: 'event', + value: 'https://hedgebox.net/signup/', + }, + ], + } + + const result = cleanEntityProperties(properties) + + expect(result).toEqual([ + { + key: '$current_url', + operator: 'exact', + type: 'event', + value: 'https://hedgebox.net/signup/', + }, + ]) + }) +}) diff --git a/frontend/src/queries/nodes/InsightQuery/utils/cleanProperties.ts b/frontend/src/queries/nodes/InsightQuery/utils/cleanProperties.ts new file mode 100644 index 0000000000000..3505204aed08a --- /dev/null +++ b/frontend/src/queries/nodes/InsightQuery/utils/cleanProperties.ts @@ -0,0 +1,180 @@ +import { + AnyPropertyFilter, + EventPropertyFilter, + PropertyFilterType, + PropertyGroupFilter, + PropertyGroupFilterValue, + PropertyOperator, +} from '~/types' + +/** Cleans properties of insights. These are either a simple list of property filters or a property group filter. The property group filter has + * a type (AND, OR) and a list of values that are property group filter values, which are either property group filter values or a simple list of + * property filters. + */ +export const cleanGlobalProperties = ( + properties: Record | Record[] | undefined +): AnyPropertyFilter[] | PropertyGroupFilter | undefined => { + if ( + properties == undefined || + (Array.isArray(properties) && properties.length == 0) || + Object.keys(properties).length == 0 + ) { + // empty properties + return undefined + } else if (isOldStyleProperties(properties)) { + // old style properties + properties = transformOldStyleProperties(properties) + properties = { + type: 'AND', + values: [{ type: 'AND', values: properties }], + } + return cleanPropertyGroupFilter(properties) + } else if (Array.isArray(properties)) { + // list of property filters + properties = { + type: 'AND', + values: [{ type: 'AND', values: properties }], + } + return cleanPropertyGroupFilter(properties) + } else if ( + (properties['type'] === 'AND' || properties['type'] === 'OR') && + !properties['values'].some((property: any) => property['type'] === 'AND' || property['type'] === 'OR') + ) { + // property group filter value + properties = { + type: 'AND', + values: [properties], + } + return cleanPropertyGroupFilter(properties) + } else { + // property group filter + return cleanPropertyGroupFilter(properties) + } +} + +/** Cleans properties of entities i.e. event and action nodes. These are a simple list of property filters. */ +export const cleanEntityProperties = ( + properties: Record | Record[] | undefined +): AnyPropertyFilter[] | undefined => { + if ( + properties == undefined || + (Array.isArray(properties) && properties.length == 0) || + Object.keys(properties).length == 0 + ) { + // empty properties + return undefined + } else if (isOldStyleProperties(properties)) { + // old style properties + return transformOldStyleProperties(properties) + } else if (Array.isArray(properties)) { + // list of property filters + return properties.map(cleanProperty) + } else if ( + (properties['type'] === 'AND' || properties['type'] === 'OR') && + !properties['values'].some((property: any) => property['type'] === 'AND' || property['type'] === 'OR') + ) { + // property group filter value + return properties.values.map(cleanProperty) + } else { + throw new Error('Unexpected format of entity properties.') + } +} + +const cleanPropertyGroupFilter = (properties: Record): PropertyGroupFilter => { + properties['values'] = cleanPropertyGroupFilterValues(properties.values) + return properties as PropertyGroupFilter +} + +const cleanPropertyGroupFilterValues = ( + properties: (AnyPropertyFilter | PropertyGroupFilterValue)[] +): (AnyPropertyFilter | PropertyGroupFilterValue)[] => { + return properties.map(cleanPropertyGroupFilterValue) +} + +const cleanPropertyGroupFilterValue = ( + property: AnyPropertyFilter | PropertyGroupFilterValue +): AnyPropertyFilter | PropertyGroupFilterValue => { + if (property['type'] == 'AND' || property['type'] == 'OR') { + // property group filter value + property['values'] = cleanPropertyGroupFilterValues(property['values'] as PropertyGroupFilterValue[]) + return property + } else { + // property filter + return cleanProperty(property) + } +} + +const cleanProperty = (property: Record): AnyPropertyFilter => { + // fix type typo + if (property['type'] === 'events') { + property['type'] = 'event' + } + + // fix value key typo + if (property['values'] !== undefined && property['value'] === undefined) { + property['value'] = property['values'] + delete property['values'] + } + + // convert precalculated and static cohorts to cohorts + if (['precalculated-cohort', 'static-cohort'].includes(property['type'])) { + property['type'] = 'cohort' + } + + // fix invalid property key for cohorts + if (property['type'] === 'cohort' && property['key'] !== 'id') { + property['key'] = 'id' + } + + // set a default operator for properties that support it, but don't have an operator set + if (isPropertyWithOperator(property) && property['operator'] === undefined) { + property['operator'] = 'exact' + } + + // remove the operator for properties that don't support it, but have it set + if (!isPropertyWithOperator(property) && property['operator'] !== undefined) { + delete property['operator'] + } + + // remove none from values + if (Array.isArray(property['value'])) { + property['value'] = property['value'].filter((x) => x !== null) + } + + // remove keys without concrete value + Object.keys(property).forEach((key) => { + if (property[key] === undefined) { + delete property[key] + } + }) + + return property +} + +const isPropertyWithOperator = (property: Record): boolean => { + return !['cohort', 'hogql'].includes(property['type']) +} + +// old style dict properties e.g. {"utm_medium__icontains": "email"} +const isOldStyleProperties = (properties: Record | Record[]): boolean => { + return ( + !Array.isArray(properties) && Object.keys(properties).length === 1 && !['AND', 'OR'].includes(properties.type) + ) +} + +const transformOldStyleProperties = ( + properties: Record | Record[] +): EventPropertyFilter[] => { + const key = Object.keys(properties)[0] + const value = Object.values(properties)[0] + const keySplit = key.split('__') + + return [ + { + key: keySplit[0], + value: value, + operator: keySplit.length > 1 ? (keySplit[1] as PropertyOperator) : PropertyOperator.Exact, + type: PropertyFilterType.Event, + }, + ] +} diff --git a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts index 12f1b95c0a10b..fea11606ec4b6 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.test.ts @@ -801,7 +801,12 @@ describe('filtersToQueryNode', () => { kind: NodeKind.LifecycleQuery, properties: { type: FilterLogicalOperator.And, - values: [], + values: [ + { + type: FilterLogicalOperator.And, + values: [], + }, + ], }, filterTestAccounts: true, dateRange: { @@ -1394,7 +1399,12 @@ describe('filtersToQueryNode', () => { }, properties: { type: FilterLogicalOperator.And, - values: [], + values: [ + { + type: FilterLogicalOperator.And, + values: [], + }, + ], }, } expect(result).toEqual(query) diff --git a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts index 40cd113a7d29d..b832a167813e2 100644 --- a/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts +++ b/frontend/src/queries/nodes/InsightQuery/utils/filtersToQueryNode.ts @@ -41,10 +41,8 @@ import { } from '~/queries/utils' import { ActionFilter, - AnyPropertyFilter, BaseMathType, DataWarehouseFilter, - FilterLogicalOperator, FilterType, FunnelExclusionLegacy, FunnelsFilterType, @@ -53,14 +51,13 @@ import { InsightType, isDataWarehouseFilter, PathsFilterType, - PropertyFilterType, - PropertyGroupFilterValue, - PropertyOperator, RetentionEntity, RetentionFilterType, TrendsFilterType, } from '~/types' +import { cleanEntityProperties, cleanGlobalProperties } from './cleanProperties' + const reverseInsightMap: Record, InsightNodeKind> = { [InsightType.TRENDS]: NodeKind.TrendsQuery, [InsightType.FUNNELS]: NodeKind.FunnelsQuery, @@ -110,7 +107,7 @@ export const legacyEntityToNode = ( } if (includeProperties) { - shared = { ...shared, properties: cleanProperties(entity.properties) } as any + shared = { ...shared, properties: cleanEntityProperties(entity.properties) } as any } if (mathAvailability !== MathAvailability.None) { @@ -213,97 +210,6 @@ export const sanitizeRetentionEntity = (entity: RetentionEntity | undefined): Re return record } -const cleanProperties = (parentProperties: FilterType['properties']): InsightsQueryBase['properties'] => { - if (!parentProperties || !parentProperties.values) { - return parentProperties - } - - const processAnyPropertyFilter = (filter: AnyPropertyFilter): AnyPropertyFilter => { - if ( - filter.type === PropertyFilterType.Event || - filter.type === PropertyFilterType.Person || - filter.type === PropertyFilterType.Element || - filter.type === PropertyFilterType.Session || - filter.type === PropertyFilterType.Group || - filter.type === PropertyFilterType.Feature || - filter.type === PropertyFilterType.Recording - ) { - return { - ...filter, - operator: filter.operator ?? PropertyOperator.Exact, - } - } - - // Some saved insights have `"operator": null` defined in the properties, this - // breaks HogQL trends and Pydantic validation - if (filter.type === PropertyFilterType.Cohort) { - if ('operator' in filter) { - delete filter.operator - } - } - - return filter - } - - const processPropertyGroupFilterValue = ( - propertyGroupFilterValue: PropertyGroupFilterValue - ): PropertyGroupFilterValue => { - if (propertyGroupFilterValue.values?.length === 0 || !propertyGroupFilterValue.values) { - return propertyGroupFilterValue - } - - // Check whether the first values type is an AND or OR - const firstValueType = propertyGroupFilterValue.values[0].type - - if (firstValueType === FilterLogicalOperator.And || firstValueType === FilterLogicalOperator.Or) { - // propertyGroupFilterValue.values is PropertyGroupFilterValue[] - const values = (propertyGroupFilterValue.values as PropertyGroupFilterValue[]).map( - processPropertyGroupFilterValue - ) - - return { - ...propertyGroupFilterValue, - values, - } - } - - // propertyGroupFilterValue.values is AnyPropertyFilter[] - const values = (propertyGroupFilterValue.values as AnyPropertyFilter[]).map(processAnyPropertyFilter) - - return { - ...propertyGroupFilterValue, - values, - } - } - - if (Array.isArray(parentProperties)) { - // parentProperties is AnyPropertyFilter[] - return parentProperties.map(processAnyPropertyFilter) - } - - if ( - (parentProperties.type === FilterLogicalOperator.And || parentProperties.type === FilterLogicalOperator.Or) && - Array.isArray(parentProperties.values) && - parentProperties.values.some( - (value) => - typeof value !== 'object' || - (value.type !== FilterLogicalOperator.And && value.type !== FilterLogicalOperator.Or) - ) - ) { - return { - type: FilterLogicalOperator.And, - values: [processPropertyGroupFilterValue(parentProperties)], - } - } - - // parentProperties is PropertyGroupFilter - const values = parentProperties.values.map(processPropertyGroupFilterValue) - return { - ...parentProperties, - values, - } -} - export const filtersToQueryNode = (filters: Partial): InsightQueryNode => { const captureException = (message: string): void => { Sentry.captureException(new Error(message), { @@ -318,7 +224,7 @@ export const filtersToQueryNode = (filters: Partial): InsightQueryNo const query: InsightsQueryBase = { kind: reverseInsightMap[filters.insight], - properties: cleanProperties(filters.properties), + properties: cleanGlobalProperties(filters.properties), filterTestAccounts: filters.filter_test_accounts, } if (filters.sampling_factor) { diff --git a/mypy-baseline.txt b/mypy-baseline.txt index b8d2d1c94da64..a60de69591cb3 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -133,11 +133,6 @@ posthog/hogql_queries/utils/query_date_range.py:0: error: Incompatible default f posthog/hogql_queries/utils/query_date_range.py:0: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True posthog/hogql_queries/utils/query_date_range.py:0: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase posthog/hogql_queries/utils/query_date_range.py:0: error: Item "None" of "IntervalType | None" has no attribute "name" [union-attr] -posthog/hogql_queries/legacy_compatibility/filter_to_query.py:0: error: Argument 1 to "filter" has incompatible type "Callable[[Any], bool]"; expected "Callable[[Any], TypeGuard[bool]]" [arg-type] -posthog/hogql_queries/legacy_compatibility/filter_to_query.py:0: error: Argument 1 to "filter" has incompatible type "Callable[[Any], bool]"; expected "Callable[[Any], TypeGuard[Any]]" [arg-type] -posthog/hogql_queries/legacy_compatibility/filter_to_query.py:0: error: Argument 2 to "filter" has incompatible type "Any | None"; expected "Iterable[Any]" [arg-type] -posthog/hogql_queries/legacy_compatibility/filter_to_query.py:0: error: Argument 2 to "map" has incompatible type "Any | None"; expected "Iterable[Any]" [arg-type] -posthog/hogql_queries/legacy_compatibility/filter_to_query.py:0: error: Argument 2 to "map" has incompatible type "Any | None"; expected "Iterable[Any]" [arg-type] posthog/hogql_queries/legacy_compatibility/filter_to_query.py:0: error: Dict entry 4 has incompatible type "str": "Literal[0, 1, 2, 3, 4] | None"; expected "str": "str | None" [dict-item] posthog/hogql_queries/legacy_compatibility/filter_to_query.py:0: error: Item "None" of "Any | None" has no attribute "__iter__" (not iterable) [union-attr] posthog/hogql_queries/legacy_compatibility/filter_to_query.py:0: error: Argument 1 to "float" has incompatible type "Any | None"; expected "str | Buffer | SupportsFloat | SupportsIndex" [arg-type] diff --git a/posthog/hogql_queries/insights/trends/test/test_trends.py b/posthog/hogql_queries/insights/trends/test/test_trends.py index 9e885fbadcc1d..1ef5c15c59b4a 100644 --- a/posthog/hogql_queries/insights/trends/test/test_trends.py +++ b/posthog/hogql_queries/insights/trends/test/test_trends.py @@ -22,7 +22,7 @@ from posthog.hogql_queries.insights.trends.trends_query_runner import TrendsQueryRunner from posthog.hogql_queries.legacy_compatibility.filter_to_query import ( clean_entity_properties, - clean_properties, + clean_global_properties, ) from posthog.models import ( Action, @@ -118,7 +118,7 @@ def _props(dict: Dict): "values": [{"type": "AND", "values": [props]}], } - return PropertyGroupFilter(**clean_properties(raw_properties)) + return PropertyGroupFilter(**clean_global_properties(raw_properties)) def convert_filter_to_trends_query(filter: Filter) -> TrendsQuery: diff --git a/posthog/hogql_queries/legacy_compatibility/clean_properties.py b/posthog/hogql_queries/legacy_compatibility/clean_properties.py new file mode 100644 index 0000000000000..27e400d8c99e3 --- /dev/null +++ b/posthog/hogql_queries/legacy_compatibility/clean_properties.py @@ -0,0 +1,133 @@ +def clean_global_properties(properties: dict | list[dict] | None): + if properties is None or len(properties) == 0: + # empty properties + return None + elif is_old_style_properties(properties): + # old style properties + properties = transform_old_style_properties(properties) + properties = { + "type": "AND", + "values": [{"type": "AND", "values": properties}], + } + return clean_property_group_filter(properties) + elif isinstance(properties, list): + # list of property filters + properties = { + "type": "AND", + "values": [{"type": "AND", "values": properties}], + } + return clean_property_group_filter(properties) + elif ( + isinstance(properties, dict) + and properties.get("type") in ["AND", "OR"] + and not any(property.get("type") in ["AND", "OR"] for property in properties["values"]) + ): + # property group filter value + properties = { + "type": "AND", + "values": [properties], + } + return clean_property_group_filter(properties) + else: + # property group filter + return clean_property_group_filter(properties) + + +def clean_entity_properties(properties: list[dict] | dict | None): + if properties is None or len(properties) == 0: + # empty properties + return None + elif is_old_style_properties(properties): + # old style properties + return transform_old_style_properties(properties) + elif isinstance(properties, list): + # list of property filters + return list(map(clean_property, properties)) + elif ( + isinstance(properties, dict) + and properties.get("type") in ["AND", "OR"] + and not any(property.get("type") in ["AND", "OR"] for property in properties["values"]) + ): + # property group filter value + return list(map(clean_property, properties["values"])) + else: + raise ValueError("Unexpected format of entity properties.") + + +def clean_property_group_filter(properties: dict): + properties["values"] = clean_property_group_filter_values(properties["values"]) + return properties + + +def clean_property_group_filter_values(properties: list[dict]): + return [clean_property_group_filter_value(property) for property in properties] + + +def clean_property_group_filter_value(property: dict): + if property.get("type") in ["AND", "OR"]: + # property group filter value + property["values"] = clean_property_group_filter_values(property["values"]) + return property + else: + # property filter + return clean_property(property) + + +def clean_property(property: dict): + cleaned_property = {**property} + + # fix type typo + if cleaned_property.get("type") == "events": + cleaned_property["type"] = "event" + + # fix value key typo + if cleaned_property.get("values") is not None and cleaned_property.get("value") is None: + cleaned_property["value"] = cleaned_property.pop("values") + + # convert precalculated and static cohorts to cohorts + if cleaned_property.get("type") in ("precalculated-cohort", "static-cohort"): + cleaned_property["type"] = "cohort" + + # fix invalid property key for cohorts + if cleaned_property.get("type") == "cohort" and cleaned_property.get("key") != "id": + cleaned_property["key"] = "id" + + # set a default operator for properties that support it, but don't have an operator set + if is_property_with_operator(cleaned_property) and cleaned_property.get("operator") is None: + cleaned_property["operator"] = "exact" + + # remove the operator for properties that don't support it, but have it set + if not is_property_with_operator(cleaned_property) and cleaned_property.get("operator") is not None: + del cleaned_property["operator"] + + # remove none from values + if isinstance(cleaned_property.get("value"), list): + cleaned_property["value"] = list(filter(lambda x: x is not None, cleaned_property.get("value"))) # type: ignore + + # remove keys without concrete value + cleaned_property = {key: value for key, value in cleaned_property.items() if value is not None} + + return cleaned_property + + +def is_property_with_operator(property: dict): + return property.get("type") not in ("cohort", "hogql") + + +# old style dict properties e.g. {"utm_medium__icontains": "email"} +def is_old_style_properties(properties): + return isinstance(properties, dict) and len(properties) == 1 and properties.get("type") not in ("AND", "OR") + + +def transform_old_style_properties(properties): + key = list(properties.keys())[0] + value = list(properties.values())[0] + key_split = key.split("__") + return [ + { + "key": key_split[0], + "value": value, + "operator": key_split[1] if len(key_split) > 1 else "exact", + "type": "event", + } + ] diff --git a/posthog/hogql_queries/legacy_compatibility/filter_to_query.py b/posthog/hogql_queries/legacy_compatibility/filter_to_query.py index 2b8f59f88a421..199294a3a5969 100644 --- a/posthog/hogql_queries/legacy_compatibility/filter_to_query.py +++ b/posthog/hogql_queries/legacy_compatibility/filter_to_query.py @@ -2,6 +2,7 @@ from enum import Enum import json from typing import List, Dict, Literal +from posthog.hogql_queries.legacy_compatibility.clean_properties import clean_entity_properties, clean_global_properties from posthog.models.entity.entity import Entity as LegacyEntity from posthog.schema import ( ActionsNode, @@ -18,7 +19,6 @@ LifecycleQuery, PathsFilter, PathsQuery, - PropertyGroupFilter, RetentionFilter, RetentionQuery, StickinessFilter, @@ -45,88 +45,6 @@ class MathAvailability(str, Enum): ] -def is_property_with_operator(property: Dict): - return property.get("type") not in ("cohort", "hogql") - - -def clean_property(property: Dict): - cleaned_property = {**property} - - # fix type typo - if cleaned_property.get("type") == "events": - cleaned_property["type"] = "event" - - # fix value key typo - if cleaned_property.get("values") is not None and cleaned_property.get("value") is None: - cleaned_property["value"] = cleaned_property.pop("values") - - # convert precalculated and static cohorts to cohorts - if cleaned_property.get("type") in ("precalculated-cohort", "static-cohort"): - cleaned_property["type"] = "cohort" - - # fix invalid property key for cohorts - if cleaned_property.get("type") == "cohort" and cleaned_property.get("key") != "id": - cleaned_property["key"] = "id" - - # set a default operator for properties that support it, but don't have an operator set - if is_property_with_operator(cleaned_property) and cleaned_property.get("operator") is None: - cleaned_property["operator"] = "exact" - - # remove the operator for properties that don't support it, but have it set - if not is_property_with_operator(cleaned_property) and cleaned_property.get("operator") is not None: - del cleaned_property["operator"] - - # remove none from values - if isinstance(cleaned_property.get("value"), List): - cleaned_property["value"] = list(filter(lambda x: x is not None, cleaned_property.get("value"))) - - # remove keys without concrete value - cleaned_property = {key: value for key, value in cleaned_property.items() if value is not None} - - return cleaned_property - - -# old style dict properties -def is_old_style_properties(properties): - return isinstance(properties, Dict) and len(properties) == 1 and properties.get("type") not in ("AND", "OR") - - -def transform_old_style_properties(properties): - key = list(properties.keys())[0] - value = list(properties.values())[0] - key_split = key.split("__") - return [ - { - "key": key_split[0], - "value": value, - "operator": key_split[1] if len(key_split) > 1 else "exact", - "type": "event", - } - ] - - -def clean_entity_properties(properties: List[Dict] | None): - if properties is None: - return None - elif is_old_style_properties(properties): - return transform_old_style_properties(properties) - else: - return list(map(clean_property, properties)) - - -def clean_property_group_filter_value(value: Dict): - if value.get("type") in ("AND", "OR"): - value["values"] = map(clean_property_group_filter_value, value.get("values")) - return value - else: - return clean_property(value) - - -def clean_properties(properties: Dict): - properties["values"] = map(clean_property_group_filter_value, properties.get("values")) - return properties - - def clean_display(display: str): if display not in ChartDisplayType.__members__: return None @@ -299,29 +217,13 @@ def _sampling_factor(filter: Dict): return {"samplingFactor": filter.get("sampling_factor")} -def _filter_test_accounts(filter: Dict): - return {"filterTestAccounts": filter.get("filter_test_accounts")} - - def _properties(filter: Dict): raw_properties = filter.get("properties", None) - if raw_properties is None or len(raw_properties) == 0: - return {} - elif isinstance(raw_properties, list): - raw_properties = { - "type": "AND", - "values": [{"type": "AND", "values": raw_properties}], - } - return {"properties": PropertyGroupFilter(**clean_properties(raw_properties))} - elif is_old_style_properties(raw_properties): - raw_properties = transform_old_style_properties(raw_properties) - raw_properties = { - "type": "AND", - "values": [{"type": "AND", "values": raw_properties}], - } - return {"properties": PropertyGroupFilter(**clean_properties(raw_properties))} - else: - return {"properties": PropertyGroupFilter(**clean_properties(raw_properties))} + return {"properties": clean_global_properties(raw_properties)} + + +def _filter_test_accounts(filter: Dict): + return {"filterTestAccounts": filter.get("filter_test_accounts")} def _breakdown_filter(_filter: Dict): diff --git a/posthog/hogql_queries/legacy_compatibility/test/test_clean_properties.py b/posthog/hogql_queries/legacy_compatibility/test/test_clean_properties.py new file mode 100644 index 0000000000000..e3e5fd91acd7b --- /dev/null +++ b/posthog/hogql_queries/legacy_compatibility/test/test_clean_properties.py @@ -0,0 +1,149 @@ +from posthog.hogql_queries.legacy_compatibility.clean_properties import clean_entity_properties, clean_global_properties +from posthog.test.base import BaseTest + + +class TestCleanGlobalProperties(BaseTest): + def test_handles_empty_properties(self): + properties: dict = {} + + result = clean_global_properties(properties) + + self.assertEqual(result, None) + + def test_handles_old_style_properties(self): + properties = {"utm_medium__icontains": "email"} + + result = clean_global_properties(properties) + + self.assertEqual( + result, + { + "type": "AND", + "values": [ + { + "type": "AND", + "values": [{"key": "utm_medium", "operator": "icontains", "type": "event", "value": "email"}], + } + ], + }, + ) + + def test_handles_property_filter_lists(self): + properties = [{"key": "id", "type": "cohort", "value": 636, "operator": None}] + + result = clean_global_properties(properties) + + self.assertEqual( + result, + { + "type": "AND", + "values": [ + { + "type": "AND", + "values": [{"key": "id", "type": "cohort", "value": 636}], + } + ], + }, + ) + + def test_handles_property_group_filters(self): + properties = { + "type": "AND", + "values": [{"type": "AND", "values": [{"key": "id", "type": "cohort", "value": 850, "operator": None}]}], + } + + result = clean_global_properties(properties) + + self.assertEqual( + result, + { + "type": "AND", + "values": [ + { + "type": "AND", + "values": [{"key": "id", "type": "cohort", "value": 850}], + } + ], + }, + ) + + def test_handles_property_group_filters_values(self): + properties = { + "type": "AND", + "values": [{"key": "id", "type": "cohort", "value": 850, "operator": None}], + } + + result = clean_global_properties(properties) + + self.assertEqual( + result, + { + "type": "AND", + "values": [ + { + "type": "AND", + "values": [{"key": "id", "type": "cohort", "value": 850}], + } + ], + }, + ) + + +class TestCleanEntityProperties(BaseTest): + def test_handles_empty_properties(self): + properties: dict = {} + + result = clean_entity_properties(properties) + + self.assertEqual(result, None) + + def test_handles_old_style_properties(self): + properties = {"utm_medium__icontains": "email"} + + result = clean_entity_properties(properties) + + self.assertEqual( + result, + [{"key": "utm_medium", "operator": "icontains", "type": "event", "value": "email"}], + ) + + def test_handles_property_filter_lists(self): + properties = [ + {"key": "$current_url", type: "event", "value": "https://hedgebox.net/signup/", "operator": "exact"}, + ] + + result = clean_entity_properties(properties) + + self.assertEqual( + result, + [ + {"key": "$current_url", type: "event", "value": "https://hedgebox.net/signup/", "operator": "exact"}, + ], + ) + + def test_handles_property_group_values(self): + properties = { + "type": "AND", + "values": [ + { + "key": "$current_url", + "operator": "exact", + "type": "event", + "value": "https://hedgebox.net/signup/", + }, + ], + } + + result = clean_entity_properties(properties) + + self.assertEqual( + result, + [ + { + "key": "$current_url", + "operator": "exact", + "type": "event", + "value": "https://hedgebox.net/signup/", + }, + ], + ) diff --git a/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py b/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py index 9421ac41be854..9abc0f3506b8c 100644 --- a/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py +++ b/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py @@ -1167,7 +1167,7 @@ def test_series_properties(self): self.assertEqual( query.series, [ - EventsNode(event="$pageview", name="$pageview", properties=[]), + EventsNode(event="$pageview", name="$pageview", properties=None), EventsNode( event="$pageview", name="$pageview",