Skip to content

Commit 0cb5c5a

Browse files
authored
feat(aci): Add ability to create new environments in the API (#112537)
# Description This allows us to send a new environment through the API; (first step for [ISWF-2363](https://linear.app/getsentry/issue/ISWF-2363/uptime-monitor-environment-selector-should-allow-you-to-create-a-new)).
1 parent 2f0f873 commit 0cb5c5a

File tree

3 files changed

+51
-3
lines changed

3 files changed

+51
-3
lines changed

src/sentry/incidents/serializers/alert_rule.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
from __future__ import annotations
2+
13
import logging
24
import operator
35
from datetime import timedelta
6+
from typing import TYPE_CHECKING
7+
8+
if TYPE_CHECKING:
9+
from sentry.models.environment import Environment
410

511
import sentry_sdk
612
from django import forms
@@ -54,7 +60,7 @@ class AlertRuleSerializer(SnubaQueryValidator, CamelSnakeModelSerializer[AlertRu
5460
- `user`: The user from `request.user`
5561
"""
5662

57-
environment = EnvironmentField(required=False, allow_null=True)
63+
environment = serializers.CharField(required=False, allow_null=True)
5864
projects = serializers.ListField(
5965
child=ProjectField(scope="project:read"),
6066
required=False,
@@ -120,6 +126,11 @@ class Meta:
120126
AlertRuleThresholdType.BELOW: lambda threshold: 100 - threshold,
121127
}
122128

129+
def validate_environment(self, value: str | None) -> Environment | None:
130+
field = EnvironmentField()
131+
field.bind("environment", self)
132+
return field.to_internal_value(value)
133+
123134
def validate_threshold_type(self, threshold_type):
124135
try:
125136
return AlertRuleThresholdType(threshold_type)

src/sentry/snuba/snuba_query_validator.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from snuba_sdk import Column, Condition, Entity, Limit, Op
1010

1111
from sentry import features
12-
from sentry.api.serializers.rest_framework import EnvironmentField
1312
from sentry.exceptions import (
1413
IncompatibleMetricsQuery,
1514
InvalidSearchQuery,
@@ -23,6 +22,7 @@
2322
translate_aggregate_field,
2423
)
2524
from sentry.incidents.utils.constants import INCIDENTS_SNUBA_SUBSCRIPTION_TYPE
25+
from sentry.models.environment import Environment
2626
from sentry.models.project import Project
2727
from sentry.search.eap.constants import VALID_GRANULARITIES
2828
from sentry.search.eap.trace_metrics.validator import validate_trace_metrics_aggregate
@@ -90,7 +90,11 @@ class SnubaQueryValidator(BaseDataSourceValidator[QuerySubscription]):
9090
query = serializers.CharField(required=True, allow_blank=True)
9191
aggregate = serializers.CharField(required=True)
9292
time_window = serializers.IntegerField(required=True)
93-
environment = EnvironmentField(required=True, allow_null=True)
93+
environment = serializers.CharField(
94+
required=True,
95+
allow_null=True,
96+
max_length=64,
97+
)
9498
event_types = serializers.ListField(
9599
child=serializers.CharField(),
96100
)
@@ -124,6 +128,23 @@ def __init__(self, *args: Any, timeWindowSeconds: bool = False, **kwargs: Any) -
124128
# TODO: only accept time_window in seconds once AlertRuleSerializer is removed
125129
self.time_window_seconds = timeWindowSeconds
126130

131+
def validate_environment(self, value: str | None) -> Environment | None:
132+
"""
133+
This is not using the `EnvironmentField` so we can inline create new envs
134+
inline when creating alerts. The use case is when a new environment is needed
135+
for an alert, but there haven't been any events ingested yet.
136+
"""
137+
if value is None:
138+
return None
139+
140+
try:
141+
return Environment.get_or_create(
142+
project=self.context["project"],
143+
name=value,
144+
)
145+
except Exception:
146+
raise serializers.ValidationError("Failed to retrieve or create environment.")
147+
127148
def validate_aggregate(self, aggregate: str) -> str:
128149
"""
129150
Reject upsampled_count() as user input. This function is reserved for internal use

tests/sentry/snuba/test_validators.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from rest_framework import serializers
33
from rest_framework.exceptions import ErrorDetail
44

5+
from sentry.models.environment import Environment
56
from sentry.snuba.dataset import Dataset
67
from sentry.snuba.models import SnubaQuery, SnubaQueryEventType
78
from sentry.snuba.snuba_query_validator import SnubaQueryValidator
@@ -39,6 +40,21 @@ def test_simple(self) -> None:
3940
assert validator.validated_data["event_types"] == [SnubaQueryEventType.EventType.ERROR]
4041
assert isinstance(validator.validated_data["_creator"], DataSourceCreator)
4142

43+
def test_environment_get_or_create(self) -> None:
44+
new_env_name = "new-test-environment"
45+
assert not Environment.objects.filter(
46+
name=new_env_name, organization_id=self.project.organization_id
47+
).exists()
48+
49+
self.valid_data["environment"] = new_env_name
50+
validator = SnubaQueryValidator(data=self.valid_data, context=self.context)
51+
assert validator.is_valid()
52+
53+
env = validator.validated_data["environment"]
54+
assert isinstance(env, Environment)
55+
assert env.name == new_env_name
56+
assert env.organization_id == self.project.organization_id
57+
4258
def test_invalid_query(self) -> None:
4359
unsupported_query = "release:latest"
4460
self.valid_data["query"] = unsupported_query

0 commit comments

Comments
 (0)