From 82a6076e36beb191f381171306a6f949b9e27b23 Mon Sep 17 00:00:00 2001 From: Nar Saynorath Date: Fri, 11 Oct 2024 15:19:30 -0400 Subject: [PATCH] fix(dashboard): OnDemand extraction for Transaction widgets Since we're splitting the datasets up, a user can create or update a widget to have DashboardWidgetTypes.TRANSACTION_LIKE as their widget type natively. Widgets like this aren't picked up by on-demand. Update the queries for on-demand widgets to also search for DashboardWidgetTypes.TRANSACTION_LIKE. --- src/sentry/relay/config/metric_extraction.py | 5 +- src/sentry/tasks/on_demand_metrics.py | 5 +- src/sentry/testutils/helpers/on_demand.py | 3 +- .../relay/config/test_metric_extraction.py | 402 +++++++++++++++--- tests/sentry/tasks/test_on_demand_metrics.py | 8 +- 5 files changed, 361 insertions(+), 62 deletions(-) diff --git a/src/sentry/relay/config/metric_extraction.py b/src/sentry/relay/config/metric_extraction.py index fa0b987964a8f9..168f67824c3169 100644 --- a/src/sentry/relay/config/metric_extraction.py +++ b/src/sentry/relay/config/metric_extraction.py @@ -258,7 +258,10 @@ def _get_widget_metric_specs( widget_queries = ( DashboardWidgetQuery.objects.filter( widget__dashboard__organization=project.organization, - widget__widget_type=DashboardWidgetTypes.DISCOVER, + widget__widget_type__in=[ + DashboardWidgetTypes.DISCOVER, + DashboardWidgetTypes.TRANSACTION_LIKE, + ], ) .prefetch_related("dashboardwidgetqueryondemand_set", "widget") .order_by("-widget__dashboard__last_visited", "widget__order") diff --git a/src/sentry/tasks/on_demand_metrics.py b/src/sentry/tasks/on_demand_metrics.py index 374c479d815858..2b38c0956ff1cb 100644 --- a/src/sentry/tasks/on_demand_metrics.py +++ b/src/sentry/tasks/on_demand_metrics.py @@ -126,7 +126,10 @@ def schedule_on_demand_check() -> None: for (widget_query_id,) in RangeQuerySetWrapper( DashboardWidgetQuery.objects.filter( - widget__widget_type=DashboardWidgetTypes.DISCOVER + widget__widget_type__in=[ + DashboardWidgetTypes.DISCOVER, + DashboardWidgetTypes.TRANSACTION_LIKE, + ] ).values_list("id"), result_value_getter=lambda item: item[0], ): diff --git a/src/sentry/testutils/helpers/on_demand.py b/src/sentry/testutils/helpers/on_demand.py index fa149f633757c0..ae131847340d95 100644 --- a/src/sentry/testutils/helpers/on_demand.py +++ b/src/sentry/testutils/helpers/on_demand.py @@ -20,6 +20,7 @@ def create_widget( dashboard: Dashboard | None = None, widget: DashboardWidget | None = None, discover_widget_split: int | None = None, + widget_type: int = DashboardWidgetTypes.DISCOVER, ) -> tuple[DashboardWidgetQuery, DashboardWidget, Dashboard]: columns = columns or [] dashboard = dashboard or Dashboard.objects.create( @@ -31,7 +32,7 @@ def create_widget( widget = widget or DashboardWidget.objects.create( dashboard=dashboard, order=order, - widget_type=DashboardWidgetTypes.DISCOVER, + widget_type=widget_type, display_type=DashboardWidgetDisplayTypes.LINE_CHART, discover_widget_split=discover_widget_split, ) diff --git a/tests/sentry/relay/config/test_metric_extraction.py b/tests/sentry/relay/config/test_metric_extraction.py index e4c3c1e5be394c..ea626f45568533 100644 --- a/tests/sentry/relay/config/test_metric_extraction.py +++ b/tests/sentry/relay/config/test_metric_extraction.py @@ -8,7 +8,11 @@ from django.utils import timezone from sentry.incidents.models.alert_rule import AlertRule, AlertRuleProjects -from sentry.models.dashboard_widget import DashboardWidgetQuery, DashboardWidgetQueryOnDemand +from sentry.models.dashboard_widget import ( + DashboardWidgetQuery, + DashboardWidgetQueryOnDemand, + DashboardWidgetTypes, +) from sentry.models.environment import Environment from sentry.models.project import Project from sentry.models.transaction_threshold import ProjectTransactionThreshold, TransactionMetric @@ -267,17 +271,29 @@ def test_get_metric_extraction_config_environment( @django_db_all -def test_get_metric_extraction_config_single_standard_widget(default_project: Project) -> None: +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) +def test_get_metric_extraction_config_single_standard_widget( + default_project: Project, widget_type: int +) -> None: with Feature({ON_DEMAND_METRICS_WIDGETS: True}): - create_widget(["count()"], "", default_project) + create_widget(["count()"], "", default_project, widget_type=widget_type) assert not get_metric_extraction_config(default_project) @django_db_all -def test_get_metric_extraction_config_single_widget(default_project: Project) -> None: +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) +def test_get_metric_extraction_config_single_widget( + default_project: Project, widget_type: int +) -> None: with Feature({ON_DEMAND_METRICS_WIDGETS: True}): - create_widget(["count()"], "transaction.duration:>=1000", default_project) + create_widget( + ["count()"], "transaction.duration:>=1000", default_project, widget_type=widget_type + ) config = get_metric_extraction_config(default_project) @@ -307,13 +323,20 @@ def test_get_metric_extraction_config_single_widget(default_project: Project) -> @django_db_all +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_get_metric_extraction_config_single_widget_multiple_aggregates( default_project: Project, + widget_type: int, ) -> None: # widget with multiple fields should result in multiple metrics with Feature({ON_DEMAND_METRICS_WIDGETS: True}): create_widget( - ["count()", "avg(transaction.duration)"], "transaction.duration:>=1000", default_project + ["count()", "avg(transaction.duration)"], + "transaction.duration:>=1000", + default_project, + widget_type=widget_type, ) config = get_metric_extraction_config(default_project) @@ -364,8 +387,12 @@ def test_get_metric_extraction_config_single_widget_multiple_aggregates( @django_db_all +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_get_metric_extraction_config_single_widget_multiple_count_if( default_project: Project, + widget_type: int, ) -> None: # widget with multiple fields should result in multiple metrics with Feature({ON_DEMAND_METRICS_WIDGETS: True}): @@ -374,7 +401,9 @@ def test_get_metric_extraction_config_single_widget_multiple_count_if( "count_if(transaction.duration, greater, 2000)", "count_if(transaction.duration, greaterOrEquals, 1000)", ] - create_widget(aggregates, "transaction.duration:>=1000", default_project) + create_widget( + aggregates, "transaction.duration:>=1000", default_project, widget_type=widget_type + ) config = get_metric_extraction_config(default_project) @@ -468,8 +497,12 @@ def test_get_metric_extraction_config_single_widget_multiple_count_if( @django_db_all +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_get_metric_extraction_config_multiple_aggregates_single_field( default_project: Project, + widget_type: int, ) -> None: # widget with multiple aggregates on the same field in a single metric with Feature({ON_DEMAND_METRICS_WIDGETS: True}): @@ -477,6 +510,7 @@ def test_get_metric_extraction_config_multiple_aggregates_single_field( ["sum(transaction.duration)", "avg(transaction.duration)"], "transaction.duration:>=1000", default_project, + widget_type=widget_type, ) config = get_metric_extraction_config(default_project) @@ -507,13 +541,27 @@ def test_get_metric_extraction_config_multiple_aggregates_single_field( @django_db_all -def test_get_metric_extraction_config_multiple_widgets_duplicated(default_project: Project) -> None: +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) +def test_get_metric_extraction_config_multiple_widgets_duplicated( + default_project: Project, widget_type: int +) -> None: # metrics should be deduplicated across widgets with Feature({ON_DEMAND_METRICS_WIDGETS: True}): create_widget( - ["count()", "avg(transaction.duration)"], "transaction.duration:>=1000", default_project + ["count()", "avg(transaction.duration)"], + "transaction.duration:>=1000", + default_project, + widget_type=widget_type, + ) + create_widget( + ["count()"], + "transaction.duration:>=1000", + default_project, + "Dashboard 2", + widget_type=widget_type, ) - create_widget(["count()"], "transaction.duration:>=1000", default_project, "Dashboard 2") config = get_metric_extraction_config(default_project) @@ -564,12 +612,24 @@ def test_get_metric_extraction_config_multiple_widgets_duplicated(default_projec @django_db_all @override_options({"on_demand.max_widget_specs": 1}) +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_get_metric_extraction_config_multiple_widgets_above_max_limit( default_project: Project, + widget_type: int, ) -> None: with Feature({ON_DEMAND_METRICS_WIDGETS: True}): - create_widget(["count()"], "transaction.duration:>=1100", default_project) - create_widget(["count()"], "transaction.duration:>=1000", default_project, "Dashboard 2") + create_widget( + ["count()"], "transaction.duration:>=1100", default_project, widget_type=widget_type + ) + create_widget( + ["count()"], + "transaction.duration:>=1000", + default_project, + "Dashboard 2", + widget_type=widget_type, + ) with mock.patch("sentry_sdk.capture_exception") as capture_exception: config = get_metric_extraction_config(default_project) @@ -589,12 +649,24 @@ def test_get_metric_extraction_config_multiple_widgets_above_max_limit( @django_db_all @override_options({"on_demand.max_widget_specs": 1}) +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_get_metric_extraction_config_multiple_widgets_not_above_max_limit_identical_hashes( default_project: Project, + widget_type: int, ) -> None: with Feature({ON_DEMAND_METRICS_WIDGETS: True}): - create_widget(["count()"], "transaction.duration:>=1000", default_project) - create_widget(["count()"], "transaction.duration:>=1000", default_project, "Dashboard 2") + create_widget( + ["count()"], "transaction.duration:>=1000", default_project, widget_type=widget_type + ) + create_widget( + ["count()"], + "transaction.duration:>=1000", + default_project, + "Dashboard 2", + widget_type=widget_type, + ) with mock.patch("sentry_sdk.capture_exception") as capture_exception: config = get_metric_extraction_config(default_project) @@ -605,17 +677,49 @@ def test_get_metric_extraction_config_multiple_widgets_not_above_max_limit_ident @django_db_all @override_options({"on_demand.max_widget_specs": 4, "on_demand_metrics.check_widgets.enable": True}) +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_get_metric_extraction_config_multiple_widgets_above_max_limit_ordered_specs( default_project: Project, + widget_type: int, ) -> None: with Feature({ON_DEMAND_METRICS_WIDGETS: True}): - create_widget(["count()"], "transaction.duration:>=1000", default_project, "Dashboard 1") - create_widget(["count()"], "transaction.duration:>=1100", default_project, "Dashboard 2") + create_widget( + ["count()"], + "transaction.duration:>=1000", + default_project, + "Dashboard 1", + widget_type=widget_type, + ) + create_widget( + ["count()"], + "transaction.duration:>=1100", + default_project, + "Dashboard 2", + widget_type=widget_type, + ) widget_query, _, _ = create_widget( - ["count()"], "transaction.duration:>=1200", default_project, "Dashboard 3" + ["count()"], + "transaction.duration:>=1200", + default_project, + "Dashboard 3", + widget_type=widget_type, + ) + create_widget( + ["count()"], + "transaction.duration:>=1300", + default_project, + "Dashboard 4", + widget_type=widget_type, + ) + create_widget( + ["count()"], + "transaction.duration:>=1400", + default_project, + "Dashboard 5", + widget_type=widget_type, ) - create_widget(["count()"], "transaction.duration:>=1300", default_project, "Dashboard 4") - create_widget(["count()"], "transaction.duration:>=1400", default_project, "Dashboard 5") widget_query.widget.dashboard.last_visited = timezone.now() - timedelta(days=1) widget_query.widget.dashboard.save() @@ -658,12 +762,24 @@ def test_get_metric_extraction_config_multiple_widgets_above_max_limit_ordered_s @django_db_all @override_options({"on_demand.max_widget_specs": 1, "on_demand.extended_max_widget_specs": 0}) +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_get_metric_extraction_config_multiple_widgets_not_using_extended_specs( default_project: Project, + widget_type: int, ) -> None: with Feature({ON_DEMAND_METRICS_WIDGETS: True}): - create_widget(["count()"], "transaction.duration:>=1100", default_project) - create_widget(["count()"], "transaction.duration:>=1000", default_project, "Dashboard 2") + create_widget( + ["count()"], "transaction.duration:>=1100", default_project, widget_type=widget_type + ) + create_widget( + ["count()"], + "transaction.duration:>=1000", + default_project, + "Dashboard 2", + widget_type=widget_type, + ) with mock.patch("sentry_sdk.capture_exception") as capture_exception: config = get_metric_extraction_config(default_project) @@ -683,8 +799,12 @@ def test_get_metric_extraction_config_multiple_widgets_not_using_extended_specs( @django_db_all @override_options({"on_demand.max_widget_specs": 0, "on_demand.extended_max_widget_specs": 1}) +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_get_metric_extraction_config_multiple_widgets_above_extended_max_limit( default_project: Project, + widget_type: int, ) -> None: with ( Feature({ON_DEMAND_METRICS_WIDGETS: True}), @@ -692,8 +812,16 @@ def test_get_metric_extraction_config_multiple_widgets_above_extended_max_limit( {"on_demand.extended_widget_spec_orgs": [default_project.organization.id]} ), ): - create_widget(["count()"], "transaction.duration:>=1100", default_project) - create_widget(["count()"], "transaction.duration:>=1000", default_project, "Dashboard 2") + create_widget( + ["count()"], "transaction.duration:>=1100", default_project, widget_type=widget_type + ) + create_widget( + ["count()"], + "transaction.duration:>=1000", + default_project, + "Dashboard 2", + widget_type=widget_type, + ) with mock.patch("sentry_sdk.capture_exception") as capture_exception: config = get_metric_extraction_config(default_project) @@ -713,8 +841,12 @@ def test_get_metric_extraction_config_multiple_widgets_above_extended_max_limit( @django_db_all @override_options({"on_demand.max_widget_specs": 0, "on_demand.extended_max_widget_specs": 2}) +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_get_metric_extraction_config_multiple_widgets_under_extended_max_limit( default_project: Project, + widget_type: int, ) -> None: with ( Feature({ON_DEMAND_METRICS_WIDGETS: True}), @@ -722,8 +854,16 @@ def test_get_metric_extraction_config_multiple_widgets_under_extended_max_limit( {"on_demand.extended_widget_spec_orgs": [default_project.organization.id]} ), ): - create_widget(["count()"], "transaction.duration:>=1100", default_project) - create_widget(["count()"], "transaction.duration:>=1000", default_project, "Dashboard 2") + create_widget( + ["count()"], "transaction.duration:>=1100", default_project, widget_type=widget_type + ) + create_widget( + ["count()"], + "transaction.duration:>=1000", + default_project, + "Dashboard 2", + widget_type=widget_type, + ) config = get_metric_extraction_config(default_project) @@ -733,11 +873,18 @@ def test_get_metric_extraction_config_multiple_widgets_under_extended_max_limit( @django_db_all -def test_get_metric_extraction_config_alerts_and_widgets_off(default_project: Project) -> None: +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) +def test_get_metric_extraction_config_alerts_and_widgets_off( + default_project: Project, widget_type: int +) -> None: # widgets should be skipped if the feature is off with Feature({ON_DEMAND_METRICS: True, ON_DEMAND_METRICS_WIDGETS: False}): create_alert("count()", "transaction.duration:>=1000", default_project) - create_widget(["count()"], "transaction.duration:>=1000", default_project) + create_widget( + ["count()"], "transaction.duration:>=1000", default_project, widget_type=widget_type + ) config = get_metric_extraction_config(default_project) @@ -756,7 +903,12 @@ def test_get_metric_extraction_config_alerts_and_widgets_off(default_project: Pr @django_db_all -def test_get_metric_extraction_config_uses_cache_for_widgets(default_project: Project) -> None: +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) +def test_get_metric_extraction_config_uses_cache_for_widgets( + default_project: Project, widget_type: int +) -> None: # widgets should be skipped if the feature is off original_set_bulk_cached_query = _set_bulk_cached_query_chunk @@ -768,7 +920,9 @@ def test_get_metric_extraction_config_uses_cache_for_widgets(default_project: Pr ) as mock_set_cache_chunk_spy, ): mock_set_cache_chunk_spy.side_effect = original_set_bulk_cached_query - create_widget(["count()"], "transaction.duration:>=1000", default_project) + create_widget( + ["count()"], "transaction.duration:>=1000", default_project, widget_type=widget_type + ) get_metric_extraction_config(default_project) @@ -779,12 +933,20 @@ def test_get_metric_extraction_config_uses_cache_for_widgets(default_project: Pr @django_db_all -def test_get_metric_extraction_config_alerts_and_widgets(default_project: Project) -> None: +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) +def test_get_metric_extraction_config_alerts_and_widgets( + default_project: Project, widget_type: int +) -> None: # deduplication should work across alerts and widgets with Feature({ON_DEMAND_METRICS_WIDGETS: True}): create_alert("count()", "transaction.duration:>=1000", default_project) create_widget( - ["count()", "avg(transaction.duration)"], "transaction.duration:>=1000", default_project + ["count()", "avg(transaction.duration)"], + "transaction.duration:>=1000", + default_project, + widget_type=widget_type, ) config = get_metric_extraction_config(default_project) @@ -835,9 +997,19 @@ def test_get_metric_extraction_config_alerts_and_widgets(default_project: Projec @django_db_all -def test_get_metric_extraction_config_with_failure_count(default_project: Project) -> None: +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) +def test_get_metric_extraction_config_with_failure_count( + default_project: Project, widget_type: int +) -> None: with Feature({ON_DEMAND_METRICS_WIDGETS: True}): - create_widget(["failure_count()"], "transaction.duration:>=1000", default_project) + create_widget( + ["failure_count()"], + "transaction.duration:>=1000", + default_project, + widget_type=widget_type, + ) config = get_metric_extraction_config(default_project) @@ -940,13 +1112,18 @@ def test_get_metric_extraction_config_with_apdex(default_project: Project) -> No "field,query_hash", [("user", "899e9132"), ("geo.city", "a85d58a1"), ("non-existent-field", "f2d80826")], ) +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_get_metric_extraction_config_with_count_unique( - default_project: Project, field: str, query_hash: str + default_project: Project, field: str, query_hash: str, widget_type: int ) -> None: duration = 1000 query = f"transaction.duration:>={duration}" with Feature({ON_DEMAND_METRICS_WIDGETS: True}): - widget_query, _, _ = create_widget([f"count_unique({field})"], query, default_project) + widget_query, _, _ = create_widget( + [f"count_unique({field})"], query, default_project, widget_type=widget_type + ) assert widget_query.aggregates == [f"count_unique({field})"] assert widget_query.conditions == query assert widget_query.columns == [] @@ -967,14 +1144,18 @@ def test_get_metric_extraction_config_with_count_unique( @django_db_all @pytest.mark.parametrize("measurement_rating", ["good", "meh", "poor", "any"]) @pytest.mark.parametrize("measurement", ["measurements.lcp"]) +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_get_metric_extraction_config_with_count_web_vitals( - default_project: Project, measurement_rating: str, measurement: str + default_project: Project, measurement_rating: str, measurement: str, widget_type: int ) -> None: with Feature({ON_DEMAND_METRICS_WIDGETS: True}): create_widget( [f"count_web_vitals({measurement}, {measurement_rating})"], "transaction.duration:>=1000", default_project, + widget_type=widget_type, ) config = get_metric_extraction_config(default_project) @@ -1195,7 +1376,12 @@ def test_get_metric_extraction_config_with_count_web_vitals( @django_db_all -def test_get_metric_extraction_config_with_user_misery(default_project: Project) -> None: +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) +def test_get_metric_extraction_config_with_user_misery( + default_project: Project, widget_type: int +) -> None: threshold = 100 duration = 1000 # User misery is extracted, querying is behind the version 2 feature flag @@ -1204,6 +1390,7 @@ def test_get_metric_extraction_config_with_user_misery(default_project: Project) [f"user_misery({threshold})"], f"transaction.duration:>={duration}", default_project, + widget_type=widget_type, ) config = get_metric_extraction_config(default_project) @@ -1248,8 +1435,12 @@ def test_get_metric_extraction_config_with_user_misery(default_project: Project) @django_db_all +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_get_metric_extraction_config_user_misery_with_tag_columns( default_project: Project, + widget_type: int, ) -> None: threshold = 100 duration = 1000 @@ -1260,6 +1451,7 @@ def test_get_metric_extraction_config_user_misery_with_tag_columns( default_project, "Dashboard", columns=["lcp.element", "custom"], + widget_type=widget_type, ) config = get_metric_extraction_config(default_project) @@ -1306,7 +1498,12 @@ def test_get_metric_extraction_config_user_misery_with_tag_columns( @django_db_all -def test_get_metric_extraction_config_epm_with_non_tag_columns(default_project: Project) -> None: +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) +def test_get_metric_extraction_config_epm_with_non_tag_columns( + default_project: Project, widget_type: int +) -> None: duration = 1000 with Feature({ON_DEMAND_METRICS_WIDGETS: True}): create_widget( @@ -1315,6 +1512,7 @@ def test_get_metric_extraction_config_epm_with_non_tag_columns(default_project: default_project, "Dashboard", columns=["user.id", "user", "release"], + widget_type=widget_type, ) config = get_metric_extraction_config(default_project) @@ -1352,7 +1550,12 @@ def test_get_metric_extraction_config_epm_with_non_tag_columns(default_project: @django_db_all @override_options({"on_demand.max_widget_cardinality.count": -1}) -def test_get_metric_extraction_config_with_high_cardinality(default_project: Project) -> None: +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) +def test_get_metric_extraction_config_with_high_cardinality( + default_project: Project, widget_type: int +) -> None: duration = 1000 with Feature({ON_DEMAND_METRICS_WIDGETS: True}): create_widget( @@ -1360,6 +1563,7 @@ def test_get_metric_extraction_config_with_high_cardinality(default_project: Pro f"transaction.duration:>={duration}", default_project, columns=["user.id", "release", "count()"], + widget_type=widget_type, ) config = get_metric_extraction_config(default_project) @@ -1368,8 +1572,12 @@ def test_get_metric_extraction_config_with_high_cardinality(default_project: Pro @django_db_all +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_get_metric_extraction_config_multiple_widgets_with_high_cardinality( default_project: Project, + widget_type: int, ) -> None: duration = 1000 with ( @@ -1385,6 +1593,7 @@ def test_get_metric_extraction_config_multiple_widgets_with_high_cardinality( default_project, columns=["user.id", "release", "count()"], title="Widget1", + widget_type=widget_type, ) create_widget( ["epm()"], @@ -1392,6 +1601,7 @@ def test_get_metric_extraction_config_multiple_widgets_with_high_cardinality( default_project, columns=["user.id", "release", "count()"], title="Widget2", + widget_type=widget_type, ) create_widget( ["epm()"], @@ -1399,6 +1609,7 @@ def test_get_metric_extraction_config_multiple_widgets_with_high_cardinality( default_project, columns=["user.id", "release", "count()"], title="Widget3", + widget_type=widget_type, ) config = get_metric_extraction_config(default_project) @@ -1410,7 +1621,12 @@ def test_get_metric_extraction_config_multiple_widgets_with_high_cardinality( @django_db_all @override_options({"on_demand.max_widget_cardinality.count": 1}) -def test_get_metric_extraction_config_with_extraction_enabled(default_project: Project) -> None: +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) +def test_get_metric_extraction_config_with_extraction_enabled( + default_project: Project, widget_type: int +) -> None: duration = 1000 with ( Feature({ON_DEMAND_METRICS_WIDGETS: True}), @@ -1428,6 +1644,7 @@ def test_get_metric_extraction_config_with_extraction_enabled(default_project: P f"transaction.duration:>={duration}", default_project, columns=["user.id", "release", "count()"], + widget_type=widget_type, ) config = get_metric_extraction_config(default_project) @@ -1442,8 +1659,12 @@ def test_get_metric_extraction_config_with_extraction_enabled(default_project: P "on_demand_metrics.widgets.use_stateful_extraction": True, } ) +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_stateful_get_metric_extraction_config_with_extraction_disabled( default_project: Project, + widget_type: int, ) -> None: duration = 1000 with ( @@ -1462,6 +1683,7 @@ def test_stateful_get_metric_extraction_config_with_extraction_disabled( f"transaction.duration:>={duration}", default_project, columns=["user.id", "release", "count()"], + widget_type=widget_type, ) config = get_metric_extraction_config(default_project) @@ -1471,8 +1693,12 @@ def test_stateful_get_metric_extraction_config_with_extraction_disabled( @django_db_all @override_options({"on_demand_metrics.widgets.use_stateful_extraction": True}) +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_stateful_get_metric_extraction_config_multiple_widgets_with_extraction_partially_disabled( default_project: Project, + widget_type: int, ) -> None: duration = 1000 with ( @@ -1492,6 +1718,7 @@ def test_stateful_get_metric_extraction_config_multiple_widgets_with_extraction_ default_project, columns=["user.id", "release", "count()"], title="Widget1", + widget_type=widget_type, ) create_widget( ["epm()"], @@ -1499,6 +1726,7 @@ def test_stateful_get_metric_extraction_config_multiple_widgets_with_extraction_ default_project, columns=["user.id", "release", "count()"], title="Widget2", + widget_type=widget_type, ) create_widget( ["epm()"], @@ -1506,6 +1734,7 @@ def test_stateful_get_metric_extraction_config_multiple_widgets_with_extraction_ default_project, columns=["user.id", "release", "count()"], title="Widget3", + widget_type=widget_type, ) config = get_metric_extraction_config(default_project) @@ -1523,8 +1752,12 @@ def test_stateful_get_metric_extraction_config_multiple_widgets_with_extraction_ "on_demand_metrics.widgets.use_stateful_extraction": True, } ) +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_stateful_get_metric_extraction_config_enabled_with_multiple_versions( default_project: Project, + widget_type: int, ) -> None: duration = 1000 with Feature( @@ -1538,6 +1771,7 @@ def test_stateful_get_metric_extraction_config_enabled_with_multiple_versions( f"transaction.duration:>={duration}", default_project, columns=["user.id", "release", "count()"], + widget_type=widget_type, ) process_widget_specs([widget_query.id]) @@ -1575,8 +1809,12 @@ def test_stateful_get_metric_extraction_config_enabled_with_multiple_versions( "on_demand_metrics.widgets.use_stateful_extraction": True, } ) +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_stateful_get_metric_extraction_config_with_low_cardinality( default_project: Project, + widget_type: int, ) -> None: duration = 1000 with Feature({ON_DEMAND_METRICS_WIDGETS: True}): @@ -1585,6 +1823,7 @@ def test_stateful_get_metric_extraction_config_with_low_cardinality( f"transaction.duration:>={duration}", default_project, columns=["user.id", "release", "count()"], + widget_type=widget_type, ) config = get_metric_extraction_config(default_project) @@ -1593,11 +1832,22 @@ def test_stateful_get_metric_extraction_config_with_low_cardinality( @django_db_all -def test_get_metric_extraction_config_with_unicode_character(default_project: Project) -> None: +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) +def test_get_metric_extraction_config_with_unicode_character( + default_project: Project, widget_type: int +) -> None: with Feature({ON_DEMAND_METRICS_WIDGETS: True}): # This will cause the Unicode bug to be raised for the current version - create_widget(["count()"], "user.name:Armén", default_project) - create_widget(["count()"], "user.name:Kevan", default_project, title="Dashboard Foo") + create_widget(["count()"], "user.name:Armén", default_project, widget_type=widget_type) + create_widget( + ["count()"], + "user.name:Kevan", + default_project, + title="Dashboard Foo", + widget_type=widget_type, + ) config = get_metric_extraction_config(default_project) assert config @@ -1654,11 +1904,14 @@ def test_get_metric_extraction_config_with_unicode_character(default_project: Pr ("epm()", "", []), ], ) +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_get_metric_extraction_config_epm_eps( - default_project: Project, metric: str, query: str, query_hashes: list[str] + default_project: Project, metric: str, query: str, query_hashes: list[str], widget_type: int ) -> None: with Feature({ON_DEMAND_METRICS_WIDGETS: True}): - create_widget([metric], query, default_project) + create_widget([metric], query, default_project, widget_type=widget_type) config = get_metric_extraction_config(default_project) @@ -1708,11 +1961,16 @@ def test_get_metric_extraction_config_epm_eps( ([], 0), # Nothing. ], ) +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_get_metrics_extraction_config_features_combinations( - enabled_features: str, number_of_metrics: int, default_project: Project + enabled_features: str, number_of_metrics: int, default_project: Project, widget_type: int ) -> None: create_alert("count()", "transaction.duration:>=10", default_project) - create_widget(["count()"], "transaction.duration:>=20", default_project) + create_widget( + ["count()"], "transaction.duration:>=20", default_project, widget_type=widget_type + ) features = {feature: True for feature in enabled_features} with Feature(features): @@ -1836,13 +2094,16 @@ def widget_to_metric_spec(query_hash: str, condition: RuleCondition | None = Non @django_db_all -def test_include_environment_for_widgets(default_project: Project) -> None: +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) +def test_include_environment_for_widgets(default_project: Project, widget_type: int) -> None: aggr = "count()" query = "transaction.duration:>=10" condition: RuleCondition = {"name": "event.duration", "op": "gte", "value": 10.0} with Feature([ON_DEMAND_METRICS, ON_DEMAND_METRICS_WIDGETS]): - widget, _, _ = create_widget([aggr], query, default_project) + widget, _, _ = create_widget([aggr], query, default_project, widget_type=widget_type) config = get_metric_extraction_config(default_project) # Because we have two specs we will have two metrics. # The second spec includes the environment tag as part of the query hash. @@ -1870,7 +2131,12 @@ def test_include_environment_for_widgets(default_project: Project) -> None: @django_db_all @override_options({"on_demand_metrics.check_widgets.enable": True}) -def test_include_environment_for_widgets_with_multiple_env(default_project: Project) -> None: +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) +def test_include_environment_for_widgets_with_multiple_env( + default_project: Project, widget_type: int +) -> None: aggrs = [ "count()", "count_unique(user)", @@ -1898,7 +2164,9 @@ def test_include_environment_for_widgets_with_multiple_env(default_project: Proj ] with Feature([ON_DEMAND_METRICS, ON_DEMAND_METRICS_WIDGETS]): - widget_query, _, _ = create_widget(aggrs, query, default_project, columns=columns) + widget_query, _, _ = create_widget( + aggrs, query, default_project, columns=columns, widget_type=widget_type + ) config = get_metric_extraction_config(default_project) assert config @@ -1943,13 +2211,16 @@ def test_include_environment_for_widgets_with_multiple_env(default_project: Proj # Remove this test once we drop the current spec version @django_db_all -def test_alert_and_widget_colliding(default_project: Project) -> None: +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) +def test_alert_and_widget_colliding(default_project: Project, widget_type: int) -> None: aggr = "count()" query = "transaction.duration:>=10" condition: RuleCondition = {"name": "event.duration", "op": "gte", "value": 10.0} with Feature([ON_DEMAND_METRICS, ON_DEMAND_METRICS_WIDGETS]): - widget, _, _ = create_widget([aggr], query, default_project) + widget, _, _ = create_widget([aggr], query, default_project, widget_type=widget_type) config = get_metric_extraction_config(default_project) # Because we have two specs we will have two metrics. assert config and config["metrics"] == [ @@ -2007,17 +2278,21 @@ def test_alert_and_widget_colliding(default_project: Project) -> None: ("foo:bar", True, ["bdb73880", "54cee1ce"], foo_bar_condition), ], ) +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_event_type( default_project: Project, query: str, config_assertion: bool, expected_hashes: list[str], expected_condition: RuleCondition | None, + widget_type: int, ) -> None: aggr = "count()" with Feature([ON_DEMAND_METRICS, ON_DEMAND_METRICS_WIDGETS]): - widget, _, _ = create_widget([aggr], query, default_project) + widget, _, _ = create_widget([aggr], query, default_project, widget_type=widget_type) config = get_metric_extraction_config(default_project) if not config_assertion: assert config is None @@ -2031,18 +2306,24 @@ def test_event_type( @django_db_all -def test_level_field(default_project: Project) -> None: +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) +def test_level_field(default_project: Project, widget_type: int) -> None: aggr = "count()" query = "level:irrelevant_value" with Feature(ON_DEMAND_METRICS_WIDGETS): - create_widget([aggr], query, default_project) + create_widget([aggr], query, default_project, widget_type=widget_type) config = get_metric_extraction_config(default_project) assert config is None @django_db_all -def test_widget_modifed_after_on_demand(default_project: Project) -> None: +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) +def test_widget_modifed_after_on_demand(default_project: Project, widget_type: int) -> None: duration = 1000 with Feature( { @@ -2055,6 +2336,7 @@ def test_widget_modifed_after_on_demand(default_project: Project) -> None: f"transaction.duration:>={duration}", default_project, columns=["user.id", "release", "count()"], + widget_type=widget_type, ) with mock.patch("sentry_sdk.capture_exception") as capture_exception: @@ -2075,8 +2357,11 @@ def test_widget_modifed_after_on_demand(default_project: Project) -> None: ], ) @django_db_all +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) def test_get_current_widget_specs( - default_project: Project, current_version: SpecVersion, expected: set[str] + default_project: Project, current_version: SpecVersion, expected: set[str], widget_type: int ) -> None: for i, (version, hashes, state) in enumerate( ( @@ -2092,6 +2377,7 @@ def test_get_current_widget_specs( default_project, title=f"Dashboard {i}", columns=["user.id", "release", "count()"], + widget_type=widget_type, ) DashboardWidgetQueryOnDemand.objects.create( dashboard_widget_query=widget_query, diff --git a/tests/sentry/tasks/test_on_demand_metrics.py b/tests/sentry/tasks/test_on_demand_metrics.py index 5fd62ceca5cc2b..9e03f49abeb792 100644 --- a/tests/sentry/tasks/test_on_demand_metrics.py +++ b/tests/sentry/tasks/test_on_demand_metrics.py @@ -4,7 +4,7 @@ import pytest -from sentry.models.dashboard_widget import DashboardWidgetQueryOnDemand +from sentry.models.dashboard_widget import DashboardWidgetQueryOnDemand, DashboardWidgetTypes from sentry.models.organization import Organization from sentry.models.project import Project from sentry.tasks import on_demand_metrics @@ -456,6 +456,9 @@ def test_schedule_on_demand_check( ) @mock.patch("sentry.tasks.on_demand_metrics._set_cardinality_cache") @mock.patch("sentry.search.events.builder.base.raw_snql_query") +@pytest.mark.parametrize( + "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] +) @django_db_all def test_process_widget_specs( raw_snql_query: Any, @@ -467,6 +470,7 @@ def test_process_widget_specs( expected_discover_queries_run: int, expected_low_cardinality: bool, project: Project, + widget_type: int, ) -> None: cache.clear() raw_snql_query.return_value = ( @@ -489,6 +493,7 @@ def test_process_widget_specs( columns=query_columns, id=2, dashboard=dashboard, + widget_type=widget_type, ) create_widget( ["count()"], @@ -497,6 +502,7 @@ def test_process_widget_specs( columns=[], id=3, dashboard=dashboard, + widget_type=widget_type, ) with override_options(options), Feature(feature_flags):