Skip to content

Commit 8190f17

Browse files
committed
feat(incidents): Add is_metric_subscription_allowed; use it
1 parent 303c904 commit 8190f17

File tree

3 files changed

+108
-111
lines changed

3 files changed

+108
-111
lines changed

src/sentry/incidents/subscription_processor.py

Lines changed: 6 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
get_comparison_aggregation_value,
1616
get_crash_rate_alert_metrics_aggregation_value_helper,
1717
)
18+
from sentry.incidents.utils.subscription_limits import is_metric_subscription_allowed
1819
from sentry.incidents.utils.types import (
1920
DATA_SOURCE_SNUBA_QUERY_SUBSCRIPTION,
2021
AnomalyDetectionUpdate,
@@ -53,46 +54,6 @@ class MetricIssueDetectorConfig(TypedDict):
5354
detection_type: Literal["static", "percent", "dynamic"]
5455

5556

56-
def has_downgraded(dataset: str, organization: Organization) -> bool:
57-
"""
58-
Check if the organization has downgraded since the subscription was created.
59-
"""
60-
supports_metrics_issues = features.has("organizations:incidents", organization)
61-
if dataset == Dataset.Events.value and not supports_metrics_issues:
62-
metrics.incr("incidents.alert_rules.ignore_update_missing_incidents")
63-
return True
64-
65-
supports_performance_view = features.has("organizations:performance-view", organization)
66-
if dataset == Dataset.Transactions.value and not (
67-
supports_metrics_issues and supports_performance_view
68-
):
69-
metrics.incr("incidents.alert_rules.ignore_update_missing_incidents_performance")
70-
return True
71-
72-
supports_explore_view = features.has("organizations:visibility-explore-view", organization)
73-
if dataset == Dataset.EventsAnalyticsPlatform.value and not (
74-
supports_metrics_issues and supports_explore_view
75-
):
76-
metrics.incr("incidents.alert_rules.ignore_update_missing_incidents_eap")
77-
return True
78-
79-
if dataset == Dataset.PerformanceMetrics.value and not features.has(
80-
"organizations:on-demand-metrics-extraction", organization
81-
):
82-
metrics.incr("incidents.alert_rules.ignore_update_missing_on_demand")
83-
return True
84-
85-
if not supports_metrics_issues:
86-
# These should probably be downgraded, but we should know the impact first.
87-
metrics.incr(
88-
"incidents.alert_rules.no_incidents_not_downgraded",
89-
sample_rate=1.0,
90-
tags={"dataset": dataset},
91-
)
92-
93-
return False
94-
95-
9657
class SubscriptionProcessor:
9758
"""
9859
Class for processing subscription updates for workflow engine.
@@ -268,7 +229,11 @@ def process_update(self, subscription_update: QuerySubscriptionUpdate) -> bool:
268229
dataset = self.subscription.snuba_query.dataset
269230
organization = self.subscription.project.organization
270231

271-
if has_downgraded(dataset, organization):
232+
if not is_metric_subscription_allowed(dataset, organization):
233+
metrics.incr(
234+
"incidents.alert_rules.ignore_update_not_enabled",
235+
tags={"dataset": dataset},
236+
)
272237
return False
273238

274239
if subscription_update["timestamp"] <= self.last_update:

src/sentry/incidents/utils/subscription_limits.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,33 @@
1+
from __future__ import annotations
2+
13
from django.conf import settings
24

3-
from sentry import options
5+
from sentry import features, options
46
from sentry.models.organization import Organization
7+
from sentry.snuba.dataset import Dataset
8+
9+
10+
def is_metric_subscription_allowed(dataset: str, organization: Organization) -> bool:
11+
"""
12+
Check whether the given organization is allowed to have a metric alert
13+
subscription for the given dataset.
14+
15+
Returns True if allowed, False if the organization lacks the required features
16+
(e.g. after a plan downgrade).
17+
"""
18+
if not features.has("organizations:incidents", organization):
19+
return False
20+
21+
if dataset == Dataset.Transactions.value:
22+
return features.has("organizations:performance-view", organization)
23+
24+
if dataset == Dataset.EventsAnalyticsPlatform.value:
25+
return features.has("organizations:visibility-explore-view", organization)
26+
27+
if dataset == Dataset.PerformanceMetrics.value:
28+
return features.has("organizations:on-demand-metrics-extraction", organization)
29+
30+
return True
531

632

733
def get_max_metric_alert_subscriptions(organization: Organization) -> int:

tests/sentry/incidents/subscription_processor/test_subscription_processor_aci.py

Lines changed: 75 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
from urllib3.response import HTTPResponse
1313

1414
from sentry.constants import ObjectStatus
15-
from sentry.incidents.subscription_processor import SubscriptionProcessor, has_downgraded
15+
from sentry.incidents.subscription_processor import SubscriptionProcessor
16+
from sentry.incidents.utils.subscription_limits import is_metric_subscription_allowed
1617
from sentry.incidents.utils.types import QuerySubscriptionUpdate
1718
from sentry.models.organization import Organization
1819
from sentry.seer.anomaly_detection.types import (
@@ -65,17 +66,18 @@ def test_missing_project(self, mock_metrics: MagicMock) -> None:
6566
self.sub.project.delete()
6667
assert self.send_update(self.critical_threshold + 1) is False
6768

68-
@patch("sentry.incidents.subscription_processor.has_downgraded", return_value=True)
69-
def test_process_update_returns_false_when_downgraded(
70-
self, mock_has_downgraded: MagicMock
71-
) -> None:
69+
@patch(
70+
"sentry.incidents.subscription_processor.is_metric_subscription_allowed",
71+
return_value=False,
72+
)
73+
def test_process_update_returns_false_when_downgraded(self, mock_is_enabled: MagicMock) -> None:
7274
message = self.build_subscription_update(
7375
self.sub, value=self.critical_threshold + 1, time_delta=timedelta()
7476
)
7577

7678
with self.capture_on_commit_callbacks(execute=True):
7779
assert SubscriptionProcessor.process(self.sub, message) is False
78-
mock_has_downgraded.assert_called_once()
80+
mock_is_enabled.assert_called_once()
7981

8082
@patch("sentry.incidents.subscription_processor.metrics")
8183
def test_invalid_aggregation_value(self, mock_metrics: MagicMock) -> None:
@@ -1014,90 +1016,94 @@ def test_ensure_case_when_no_metrics_index_not_found_is_handled_gracefully(
10141016
)
10151017

10161018

1017-
@patch("sentry.incidents.subscription_processor.metrics")
1018-
class TestHasDowngraded:
1019+
class TestIsMetricSubscriptionAllowed:
10191020
org = MagicMock(spec=Organization)
10201021

10211022
@contextmanager
10221023
def fake_features(self, enabled: set[str]):
1023-
with patch("sentry.incidents.subscription_processor.features") as mock_features:
1024+
with patch("sentry.incidents.utils.subscription_limits.features") as mock_features:
10241025
mock_features.has.side_effect = lambda name, *a, **kw: name in enabled
10251026
yield
10261027

1027-
def test_events_without_incidents_feature(self, mock_metrics: MagicMock) -> None:
1028+
# -- :incidents is the universal gate --
1029+
1030+
def test_no_incidents_disables_events(self) -> None:
10281031
with self.fake_features(set()):
1029-
assert has_downgraded(Dataset.Events.value, self.org) is True
1030-
mock_metrics.incr.assert_called_with(
1031-
"incidents.alert_rules.ignore_update_missing_incidents"
1032-
)
1032+
assert is_metric_subscription_allowed(Dataset.Events.value, self.org) is False
10331033

1034-
def test_events_with_incidents_feature(self, mock_metrics: MagicMock) -> None:
1035-
with self.fake_features({"organizations:incidents"}):
1036-
assert has_downgraded(Dataset.Events.value, self.org) is False
1034+
def test_no_incidents_disables_transactions(self) -> None:
1035+
with self.fake_features({"organizations:performance-view"}):
1036+
assert is_metric_subscription_allowed(Dataset.Transactions.value, self.org) is False
10371037

1038-
def test_transactions_without_any_features(self, mock_metrics: MagicMock) -> None:
1038+
def test_no_incidents_disables_eap(self) -> None:
1039+
with self.fake_features({"organizations:visibility-explore-view"}):
1040+
assert (
1041+
is_metric_subscription_allowed(Dataset.EventsAnalyticsPlatform.value, self.org)
1042+
is False
1043+
)
1044+
1045+
def test_no_incidents_disables_performance_metrics(self) -> None:
1046+
with self.fake_features({"organizations:on-demand-metrics-extraction"}):
1047+
assert (
1048+
is_metric_subscription_allowed(Dataset.PerformanceMetrics.value, self.org) is False
1049+
)
1050+
1051+
def test_no_incidents_disables_unknown_dataset(self) -> None:
10391052
with self.fake_features(set()):
1040-
assert has_downgraded(Dataset.Transactions.value, self.org) is True
1041-
mock_metrics.incr.assert_called_with(
1042-
"incidents.alert_rules.ignore_update_missing_incidents_performance"
1043-
)
1053+
assert is_metric_subscription_allowed("unknown_dataset", self.org) is False
10441054

1045-
def test_transactions_with_only_performance_view(self, mock_metrics: MagicMock) -> None:
1046-
with self.fake_features({"organizations:performance-view"}):
1047-
assert has_downgraded(Dataset.Transactions.value, self.org) is True
1048-
mock_metrics.incr.assert_called_with(
1049-
"incidents.alert_rules.ignore_update_missing_incidents_performance"
1050-
)
1055+
# -- Events --
1056+
1057+
def test_events_enabled(self) -> None:
1058+
with self.fake_features({"organizations:incidents"}):
1059+
assert is_metric_subscription_allowed(Dataset.Events.value, self.org) is True
1060+
1061+
# -- Transactions --
1062+
1063+
def test_transactions_without_performance_view(self) -> None:
1064+
with self.fake_features({"organizations:incidents"}):
1065+
assert is_metric_subscription_allowed(Dataset.Transactions.value, self.org) is False
10511066

1052-
def test_transactions_with_both_features(self, mock_metrics: MagicMock) -> None:
1067+
def test_transactions_with_both_features(self) -> None:
10531068
with self.fake_features({"organizations:incidents", "organizations:performance-view"}):
1054-
assert has_downgraded(Dataset.Transactions.value, self.org) is False
1069+
assert is_metric_subscription_allowed(Dataset.Transactions.value, self.org) is True
10551070

1056-
def test_eap_without_features(self, mock_metrics: MagicMock) -> None:
1057-
with self.fake_features(set()):
1058-
assert has_downgraded(Dataset.EventsAnalyticsPlatform.value, self.org) is True
1059-
mock_metrics.incr.assert_called_with(
1060-
"incidents.alert_rules.ignore_update_missing_incidents_eap"
1061-
)
1071+
# -- EAP --
10621072

1063-
def test_eap_with_only_explore_view(self, mock_metrics: MagicMock) -> None:
1064-
with self.fake_features({"organizations:visibility-explore-view"}):
1065-
assert has_downgraded(Dataset.EventsAnalyticsPlatform.value, self.org) is True
1066-
mock_metrics.incr.assert_called_with(
1067-
"incidents.alert_rules.ignore_update_missing_incidents_eap"
1068-
)
1073+
def test_eap_without_explore_view(self) -> None:
1074+
with self.fake_features({"organizations:incidents"}):
1075+
assert (
1076+
is_metric_subscription_allowed(Dataset.EventsAnalyticsPlatform.value, self.org)
1077+
is False
1078+
)
10691079

1070-
def test_eap_with_both_features(self, mock_metrics: MagicMock) -> None:
1080+
def test_eap_with_both_features(self) -> None:
10711081
with self.fake_features(
10721082
{"organizations:incidents", "organizations:visibility-explore-view"}
10731083
):
1074-
assert has_downgraded(Dataset.EventsAnalyticsPlatform.value, self.org) is False
1084+
assert (
1085+
is_metric_subscription_allowed(Dataset.EventsAnalyticsPlatform.value, self.org)
1086+
is True
1087+
)
10751088

1076-
def test_performance_metrics_without_on_demand_feature(self, mock_metrics: MagicMock) -> None:
1077-
with self.fake_features(set()):
1078-
assert has_downgraded(Dataset.PerformanceMetrics.value, self.org) is True
1079-
mock_metrics.incr.assert_called_with(
1080-
"incidents.alert_rules.ignore_update_missing_on_demand"
1081-
)
1089+
# -- PerformanceMetrics --
10821090

1083-
def test_performance_metrics_with_on_demand_feature(self, mock_metrics: MagicMock) -> None:
1084-
with self.fake_features({"organizations:on-demand-metrics-extraction"}):
1085-
assert has_downgraded(Dataset.PerformanceMetrics.value, self.org) is False
1091+
def test_performance_metrics_without_on_demand(self) -> None:
1092+
with self.fake_features({"organizations:incidents"}):
1093+
assert (
1094+
is_metric_subscription_allowed(Dataset.PerformanceMetrics.value, self.org) is False
1095+
)
10861096

1087-
def test_unknown_dataset_not_downgraded(self, mock_metrics: MagicMock) -> None:
1088-
with self.fake_features(set()):
1089-
assert has_downgraded("unknown_dataset", self.org) is False
1090-
mock_metrics.incr.assert_called_with(
1091-
"incidents.alert_rules.no_incidents_not_downgraded",
1092-
sample_rate=1.0,
1093-
tags={"dataset": "unknown_dataset"},
1094-
)
1097+
def test_performance_metrics_with_both_features(self) -> None:
1098+
with self.fake_features(
1099+
{"organizations:incidents", "organizations:on-demand-metrics-extraction"}
1100+
):
1101+
assert (
1102+
is_metric_subscription_allowed(Dataset.PerformanceMetrics.value, self.org) is True
1103+
)
10951104

1096-
def test_no_incidents_not_downgraded_emits_metric(self, mock_metrics: MagicMock) -> None:
1097-
with self.fake_features({"organizations:on-demand-metrics-extraction"}):
1098-
assert has_downgraded(Dataset.PerformanceMetrics.value, self.org) is False
1099-
mock_metrics.incr.assert_called_with(
1100-
"incidents.alert_rules.no_incidents_not_downgraded",
1101-
sample_rate=1.0,
1102-
tags={"dataset": Dataset.PerformanceMetrics.value},
1103-
)
1105+
# -- Unknown / other datasets --
1106+
1107+
def test_unknown_dataset_with_incidents(self) -> None:
1108+
with self.fake_features({"organizations:incidents"}):
1109+
assert is_metric_subscription_allowed("unknown_dataset", self.org) is True

0 commit comments

Comments
 (0)