Skip to content

Commit 8e34b54

Browse files
evanpurkhisercursoragentgetsantry[bot]
authored
Add single datasource enforcement config (#103870)
<!-- Describe your PR here. --> Introduces `enforce_single_datasource` on the base detector validator to restrict specific detector types (Uptime and Crons) to a single data source. This prevents invalid configurations where these detectors might be set up with multiple data sources, which is not supported for their intended functionality. <!-- Sentry employees and contractors can delete or ignore the following. --> ### Legal Boilerplate Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms. --- [Slack Thread](https://sentry.slack.com/archives/C04P3P6669X/p1763769018150559?thread_ts=1763769018.150559&cid=C04P3P6669X) <a href="https://cursor.com/background-agent?bcId=bc-6e226670-3a54-442a-8e34-be99f595b1ba"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-cursor-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-cursor-light.svg"><img alt="Open in Cursor" src="https://cursor.com/open-in-cursor.svg"></picture></a>&nbsp;<a href="https://cursor.com/agents?id=bc-6e226670-3a54-442a-8e34-be99f595b1ba"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/open-in-web-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/open-in-web-light.svg"><img alt="Open in Web" src="https://cursor.com/open-in-web.svg"></picture></a> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent b556326 commit 8e34b54

File tree

5 files changed

+60
-0
lines changed

5 files changed

+60
-0
lines changed

src/sentry/monitors/validators.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,7 @@ class MonitorIncidentDetectorValidator(BaseDetectorTypeValidator):
716716
data_source field (MonitorDataSourceValidator).
717717
"""
718718

719+
enforce_single_datasource = True
719720
data_sources = MonitorDataSourceListField(child=MonitorDataSourceValidator(), required=False)
720721

721722
def validate_enabled(self, value: bool) -> bool:

src/sentry/uptime/endpoints/validators.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,7 @@ def create_source(self, validated_data: dict[str, Any]) -> UptimeSubscription:
464464

465465

466466
class UptimeDomainCheckFailureValidator(BaseDetectorTypeValidator):
467+
enforce_single_datasource = True
467468
data_sources = serializers.ListField(child=UptimeMonitorDataSourceValidator(), required=False)
468469

469470
def validate_config(self, config: dict[str, Any]) -> dict[str, Any]:

src/sentry/workflow_engine/endpoints/validators/base/detector.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ class DetectorQuota:
4141

4242

4343
class BaseDetectorTypeValidator(CamelSnakeSerializer):
44+
enforce_single_datasource = False
45+
"""
46+
Set to True in subclasses to enforce that only a single data source can be configured.
47+
This prevents invalid configurations for detector types that don't support multiple data sources.
48+
"""
49+
4450
name = serializers.CharField(
4551
required=True,
4652
max_length=200,
@@ -79,6 +85,16 @@ def data_sources(self) -> serializers.ListField:
7985
def data_conditions(self) -> BaseDataConditionValidator:
8086
raise NotImplementedError
8187

88+
def validate_data_sources(self, value: list[dict[str, Any]]) -> list[dict[str, Any]]:
89+
"""
90+
Validate data sources, enforcing single data source if configured.
91+
"""
92+
if self.enforce_single_datasource and len(value) > 1:
93+
raise serializers.ValidationError(
94+
"Only one data source is allowed for this detector type."
95+
)
96+
return value
97+
8298
def get_quota(self) -> DetectorQuota:
8399
return DetectorQuota(has_exceeded=False, limit=-1, count=-1)
84100

tests/sentry/monitors/test_validators.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,27 @@ def test_detector_requires_data_source(self):
11951195
assert not validator.is_valid()
11961196
assert "dataSources" in validator.errors
11971197

1198+
def test_rejects_multiple_data_sources(self):
1199+
"""Test that multiple data sources are rejected for cron monitors."""
1200+
data = self._get_valid_detector_data(
1201+
dataSources=[
1202+
{
1203+
"name": "Test Monitor 1",
1204+
"slug": "test-monitor-1",
1205+
"config": self._get_base_config(),
1206+
},
1207+
{
1208+
"name": "Test Monitor 2",
1209+
"slug": "test-monitor-2",
1210+
"config": self._get_base_config(),
1211+
},
1212+
]
1213+
)
1214+
validator = self._create_validator(data)
1215+
assert not validator.is_valid()
1216+
assert "dataSources" in validator.errors
1217+
assert "Only one data source is allowed" in str(validator.errors["dataSources"])
1218+
11981219
def test_create_detector_validates_data_source(self):
11991220
condition_group = DataConditionGroup.objects.create(
12001221
organization_id=self.organization.id,

tests/sentry/uptime/endpoints/test_validators.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,27 @@ def get_valid_data(self, **kwargs):
146146
),
147147
}
148148

149+
def test_rejects_multiple_data_sources(self):
150+
"""Test that multiple data sources are rejected for uptime monitors."""
151+
data = self.get_valid_data(
152+
data_sources=[
153+
{
154+
"url": "https://sentry.io",
155+
"intervalSeconds": 60,
156+
"timeoutMs": 1000,
157+
},
158+
{
159+
"url": "https://example.com",
160+
"intervalSeconds": 60,
161+
"timeoutMs": 1000,
162+
},
163+
]
164+
)
165+
validator = UptimeDomainCheckFailureValidator(data=data, context=self.context)
166+
assert not validator.is_valid()
167+
assert "dataSources" in validator.errors
168+
assert "Only one data source is allowed" in str(validator.errors["dataSources"])
169+
149170
@mock.patch(
150171
"sentry.quotas.backend.assign_seat",
151172
return_value=Outcome.ACCEPTED,

0 commit comments

Comments
 (0)