From b440df333bb325fdd2e99f3e69fcdcff281199da Mon Sep 17 00:00:00 2001 From: Cathy Teng Date: Mon, 24 Nov 2025 15:48:47 -0800 Subject: [PATCH 1/3] always combine rule and workflow fire history in API --- src/sentry/rules/history/backends/postgres.py | 97 +++++++++---------- .../rules/history/backends/test_postgres.py | 1 - 2 files changed, 47 insertions(+), 51 deletions(-) diff --git a/src/sentry/rules/history/backends/postgres.py b/src/sentry/rules/history/backends/postgres.py index 4b15ab467e663e..82f028c1aed2d0 100644 --- a/src/sentry/rules/history/backends/postgres.py +++ b/src/sentry/rules/history/backends/postgres.py @@ -69,60 +69,57 @@ def fetch_rule_groups_paginated( cursor: Cursor | None = None, per_page: int = 25, ) -> CursorResult[RuleGroupHistory]: - if features.has( - "organizations:workflow-engine-single-process-workflows", rule.project.organization - ): - try: - alert_rule_workflow = AlertRuleWorkflow.objects.get(rule_id=rule.id) - workflow = alert_rule_workflow.workflow - - # Performs the raw SQL query with pagination - def data_fn(offset: int, limit: int) -> list[_Result]: - query = """ - WITH combined_data AS ( - SELECT group_id, date_added, event_id - FROM sentry_rulefirehistory - WHERE rule_id = %s AND date_added >= %s AND date_added < %s - UNION ALL - SELECT group_id, date_added, event_id - FROM workflow_engine_workflowfirehistory - WHERE workflow_id = %s - AND date_added >= %s AND date_added < %s - ) - SELECT - group_id as group, - COUNT(*) as count, - MAX(date_added) as last_triggered, - (ARRAY_AGG(event_id ORDER BY date_added DESC))[1] as event_id - FROM combined_data - GROUP BY group_id - ORDER BY count DESC, last_triggered DESC - LIMIT %s OFFSET %s - """ - - with connection.cursor() as cursor: - cursor.execute( - query, [rule.id, start, end, workflow.id, start, end, limit, offset] + try: + alert_rule_workflow = AlertRuleWorkflow.objects.get(rule_id=rule.id) + workflow = alert_rule_workflow.workflow + + # Performs the raw SQL query with pagination + def data_fn(offset: int, limit: int) -> list[_Result]: + query = """ + WITH combined_data AS ( + SELECT group_id, date_added, event_id + FROM sentry_rulefirehistory + WHERE rule_id = %s AND date_added >= %s AND date_added < %s + UNION ALL + SELECT group_id, date_added, event_id + FROM workflow_engine_workflowfirehistory + WHERE workflow_id = %s + AND date_added >= %s AND date_added < %s + ) + SELECT + group_id as group, + COUNT(*) as count, + MAX(date_added) as last_triggered, + (ARRAY_AGG(event_id ORDER BY date_added DESC))[1] as event_id + FROM combined_data + GROUP BY group_id + ORDER BY count DESC, last_triggered DESC + LIMIT %s OFFSET %s + """ + + with connection.cursor() as cursor: + cursor.execute( + query, [rule.id, start, end, workflow.id, start, end, limit, offset] + ) + return [ + _Result( + group=row[0], + count=row[1], + last_triggered=row[2], + event_id=row[3], ) - return [ - _Result( - group=row[0], - count=row[1], - last_triggered=row[2], - event_id=row[3], - ) - for row in cursor.fetchall() - ] + for row in cursor.fetchall() + ] - result = GenericOffsetPaginator(data_fn=data_fn).get_result(per_page, cursor) - result.results = convert_results(result.results) + result = GenericOffsetPaginator(data_fn=data_fn).get_result(per_page, cursor) + result.results = convert_results(result.results) - return result + return result - except AlertRuleWorkflow.DoesNotExist: - # If no workflow is associated with this rule, just use the original behavior - logger.exception("No workflow associated with rule", extra={"rule_id": rule.id}) - pass + except AlertRuleWorkflow.DoesNotExist: + # If no workflow is associated with this rule, just use the original behavior + logger.exception("No workflow associated with rule", extra={"rule_id": rule.id}) + pass rule_filtered_history = RuleFireHistory.objects.filter( rule=rule, diff --git a/tests/sentry/rules/history/backends/test_postgres.py b/tests/sentry/rules/history/backends/test_postgres.py index 45a452be4cc2e4..29af23386e8d1e 100644 --- a/tests/sentry/rules/history/backends/test_postgres.py +++ b/tests/sentry/rules/history/backends/test_postgres.py @@ -196,7 +196,6 @@ def test_event_id(self) -> None: ], ) - @with_feature("organizations:workflow-engine-single-process-workflows") def test_combined_rule_and_workflow_history(self) -> None: """Test combining RuleFireHistory and WorkflowFireHistory when feature flag is enabled""" rule = self.create_project_rule(project=self.event.project) From 8ea2b1193bf0fe2e5fa1e658f31cb90c90103efd Mon Sep 17 00:00:00 2001 From: Cathy Teng Date: Mon, 24 Nov 2025 15:49:18 -0800 Subject: [PATCH 2/3] fix rule serializer too --- src/sentry/api/serializers/models/rule.py | 6 +----- tests/sentry/api/serializers/test_rule.py | 3 --- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/sentry/api/serializers/models/rule.py b/src/sentry/api/serializers/models/rule.py index e702ca57507b36..aaf64de7d95ed0 100644 --- a/src/sentry/api/serializers/models/rule.py +++ b/src/sentry/api/serializers/models/rule.py @@ -9,7 +9,6 @@ from django.db.models import Max, Prefetch, Q, prefetch_related_objects from rest_framework import serializers -from sentry import features from sentry.api.serializers import Serializer, register from sentry.constants import ObjectStatus from sentry.db.models.manager.base_query_set import BaseQuerySet @@ -210,10 +209,7 @@ def get_attrs(self, item_list, user, **kwargs): } # Update lastTriggered with WorkflowFireHistory if available - if item_list and features.has( - "organizations:workflow-engine-single-process-workflows", - item_list[0].project.organization, - ): + if item_list: rule_ids = [rule.id for rule in item_list] workflow_rule_lookup = dict( AlertRuleWorkflow.objects.filter(rule_id__in=rule_ids).values_list( diff --git a/tests/sentry/api/serializers/test_rule.py b/tests/sentry/api/serializers/test_rule.py index e142d1c7b1d095..5479592031bffe 100644 --- a/tests/sentry/api/serializers/test_rule.py +++ b/tests/sentry/api/serializers/test_rule.py @@ -14,7 +14,6 @@ from sentry.rules.filters.tagged_event import TaggedEventFilter from sentry.testutils.cases import TestCase from sentry.testutils.helpers.datetime import before_now, freeze_time -from sentry.testutils.helpers.features import with_feature from sentry.users.services.user.serial import serialize_rpc_user from sentry.workflow_engine.migration_helpers.issue_alert_migration import IssueAlertMigrator from sentry.workflow_engine.models import WorkflowDataConditionGroup, WorkflowFireHistory @@ -36,7 +35,6 @@ def test_last_triggered_rule_only(self) -> None: result = serialize(rule, self.user, RuleSerializer(expand=["lastTriggered"])) assert result["lastTriggered"] == timezone.now() - @with_feature("organizations:workflow-engine-single-process-workflows") def test_last_triggered_with_workflow_only(self) -> None: rule = self.create_project_rule() @@ -50,7 +48,6 @@ def test_last_triggered_with_workflow_only(self) -> None: result = serialize(rule, self.user, RuleSerializer(expand=["lastTriggered"])) assert result["lastTriggered"] == timezone.now() - @with_feature("organizations:workflow-engine-single-process-workflows") def test_last_triggered_with_workflow(self) -> None: rule = self.create_project_rule() From d9674ab8be00e77105befd64d5ffff79b8051542 Mon Sep 17 00:00:00 2001 From: Cathy Teng Date: Mon, 24 Nov 2025 15:53:18 -0800 Subject: [PATCH 3/3] also remove from hourly stats --- src/sentry/rules/history/backends/postgres.py | 80 +++++++++---------- .../rules/history/backends/test_postgres.py | 2 - 2 files changed, 38 insertions(+), 44 deletions(-) diff --git a/src/sentry/rules/history/backends/postgres.py b/src/sentry/rules/history/backends/postgres.py index 82f028c1aed2d0..7f1b760a9113ba 100644 --- a/src/sentry/rules/history/backends/postgres.py +++ b/src/sentry/rules/history/backends/postgres.py @@ -9,7 +9,6 @@ from django.db.models import Count, Max, OuterRef, Subquery from django.db.models.functions import TruncHour -from sentry import features from sentry.api.paginator import GenericOffsetPaginator, OffsetPaginator from sentry.models.group import Group from sentry.models.rulefirehistory import RuleFireHistory @@ -151,50 +150,47 @@ def fetch_rule_hourly_stats( existing_data: dict[datetime, TimeSeriesValue] = {} - if features.has( - "organizations:workflow-engine-single-process-workflows", rule.project.organization - ): - try: - alert_rule_workflow = AlertRuleWorkflow.objects.get(rule_id=rule.id) - workflow = alert_rule_workflow.workflow - - # Use raw SQL to combine data from both tables - with connection.cursor() as db_cursor: - db_cursor.execute( - """ - SELECT - DATE_TRUNC('hour', date_added) as bucket, - COUNT(*) as count - FROM ( - SELECT date_added - FROM sentry_rulefirehistory - WHERE rule_id = %s - AND date_added >= %s - AND date_added < %s - - UNION ALL - - SELECT date_added - FROM workflow_engine_workflowfirehistory - WHERE workflow_id = %s - AND date_added >= %s - AND date_added < %s - ) combined_data - GROUP BY DATE_TRUNC('hour', date_added) - ORDER BY bucket - """, - [rule.id, start, end, workflow.id, start, end], - ) + try: + alert_rule_workflow = AlertRuleWorkflow.objects.get(rule_id=rule.id) + workflow = alert_rule_workflow.workflow - results = db_cursor.fetchall() + # Use raw SQL to combine data from both tables + with connection.cursor() as db_cursor: + db_cursor.execute( + """ + SELECT + DATE_TRUNC('hour', date_added) as bucket, + COUNT(*) as count + FROM ( + SELECT date_added + FROM sentry_rulefirehistory + WHERE rule_id = %s + AND date_added >= %s + AND date_added < %s - # Convert raw SQL results to the expected format - existing_data = {row[0]: TimeSeriesValue(row[0], row[1]) for row in results} + UNION ALL - except AlertRuleWorkflow.DoesNotExist: - # If no workflow is associated with this rule, just use the original behavior - logger.exception("No workflow associated with rule", extra={"rule_id": rule.id}) - pass + SELECT date_added + FROM workflow_engine_workflowfirehistory + WHERE workflow_id = %s + AND date_added >= %s + AND date_added < %s + ) combined_data + GROUP BY DATE_TRUNC('hour', date_added) + ORDER BY bucket + """, + [rule.id, start, end, workflow.id, start, end], + ) + + results = db_cursor.fetchall() + + # Convert raw SQL results to the expected format + existing_data = {row[0]: TimeSeriesValue(row[0], row[1]) for row in results} + + except AlertRuleWorkflow.DoesNotExist: + # If no workflow is associated with this rule, just use the original behavior + logger.exception("No workflow associated with rule", extra={"rule_id": rule.id}) + pass if not existing_data: qs = ( diff --git a/tests/sentry/rules/history/backends/test_postgres.py b/tests/sentry/rules/history/backends/test_postgres.py index 29af23386e8d1e..ea23725eee0057 100644 --- a/tests/sentry/rules/history/backends/test_postgres.py +++ b/tests/sentry/rules/history/backends/test_postgres.py @@ -6,7 +6,6 @@ from sentry.rules.history.base import RuleGroupHistory from sentry.testutils.cases import TestCase from sentry.testutils.helpers.datetime import before_now, freeze_time -from sentry.testutils.helpers.features import with_feature from sentry.testutils.skips import requires_snuba from sentry.workflow_engine.models import AlertRuleWorkflow, WorkflowFireHistory @@ -334,7 +333,6 @@ def test(self) -> None: assert len(results) == 24 assert [r.count for r in results[-5:]] == [0, 0, 1, 1, 0] - @with_feature("organizations:workflow-engine-single-process-workflows") def test_combined_rule_and_workflow_history(self) -> None: """Test combining RuleFireHistory and WorkflowFireHistory for hourly stats when feature flag is enabled""" rule = self.create_project_rule(project=self.event.project)