Skip to content

Commit

Permalink
feat(Activated Alerts): implement deploy activator (#70712)
Browse files Browse the repository at this point in the history
Deploy reference a specific release being deployed to a specific
environment

To activate a monitor for a deploy means to start monitoring per release
AND environment for a specific Project.

This PR subscribes the monitor subscription to the creation of a
`ReleaseProjectEnvironment` which for all intents and purposes is
synonymous with "deploy"
  • Loading branch information
nhsiehgit committed May 14, 2024
1 parent 1f61c87 commit 6321bd8
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 11 deletions.
13 changes: 3 additions & 10 deletions src/sentry/models/deploy.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
from sentry.backup.scopes import RelocationScope
from sentry.db.models import region_silo_model

"""
sentry.models.deploy
~~~~~~~~~~~~~~~~~~~~
"""


from django.db import models
from django.utils import timezone

from sentry.backup.scopes import RelocationScope
from sentry.db.models import (
BoundedBigIntegerField,
BoundedPositiveIntegerField,
FlexibleForeignKey,
Model,
region_silo_model,
)
from sentry.locks import locks
from sentry.models.environment import Environment
from sentry.types.activity import ActivityType
from sentry.utils.retries import TimedRetryPolicy

Expand Down Expand Up @@ -49,7 +43,6 @@ def notify_if_ready(cls, deploy_id, fetch_complete=False):
if they haven't been sent
"""
from sentry.models.activity import Activity
from sentry.models.environment import Environment
from sentry.models.releasecommit import ReleaseCommit
from sentry.models.releaseheadcommit import ReleaseHeadCommit

Expand Down
49 changes: 49 additions & 0 deletions src/sentry/models/releaseprojectenvironment.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from __future__ import annotations

from datetime import timedelta
from enum import Enum
from typing import TYPE_CHECKING, ClassVar

from django.db import models
from django.utils import timezone
Expand All @@ -12,16 +15,60 @@
region_silo_model,
sane_repr,
)
from sentry.db.models.manager import BaseManager
from sentry.incidents.utils.types import AlertRuleActivationConditionType
from sentry.utils import metrics
from sentry.utils.cache import cache

if TYPE_CHECKING:
from sentry.models.environment import Environment
from sentry.models.project import Project
from sentry.models.release import Release
from sentry.snuba.models import QuerySubscription


class ReleaseStages(str, Enum):
ADOPTED = "adopted"
LOW_ADOPTION = "low_adoption"
REPLACED = "replaced"


class ReleaseProjectEnvironmentManager(BaseManager["ReleaseProjectEnvironment"]):
@staticmethod
def subscribe_project_to_alert_rule(
project: Project, release: Release, environment: Environment, trigger: str
) -> list[QuerySubscription]:
"""
TODO: potentially enable custom query_extra to be passed on ReleaseProject creation (on release/deploy)
NOTE: import AlertRule model here to avoid circular dependency
"""
from sentry.incidents.models.alert_rule import AlertRule

query_extra = f"release:{release.version} and environment:{environment.name}"
# TODO: parse activator on the client to derive release version / environment name
activator = f"release:{release.version} and environment:{environment.name}"
return AlertRule.objects.conditionally_subscribe_project_to_alert_rules(
project=project,
activation_condition=AlertRuleActivationConditionType.DEPLOY_CREATION,
query_extra=query_extra,
origin=trigger,
activator=activator,
)

def post_save(self, instance, created, **kwargs):
if created:
release = instance.release
project = instance.project
environment = instance.environment
self.subscribe_project_to_alert_rule(
project=project,
release=release,
environment=environment,
trigger="releaseprojectenvironment.post_save",
)


@region_silo_model
class ReleaseProjectEnvironment(Model):
__relocation_scope__ = RelocationScope.Excluded
Expand All @@ -37,6 +84,8 @@ class ReleaseProjectEnvironment(Model):
adopted = models.DateTimeField(null=True, blank=True)
unadopted = models.DateTimeField(null=True, blank=True)

objects: ClassVar[ReleaseProjectEnvironmentManager] = ReleaseProjectEnvironmentManager()

class Meta:
app_label = "sentry"
db_table = "sentry_releaseprojectenvironment"
Expand Down
4 changes: 4 additions & 0 deletions static/app/views/alerts/rules/metric/ruleConditionsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,10 @@ class RuleConditionsForm extends PureComponent<Props, State> {
value: ActivationConditionType.RELEASE_CREATION,
label: t('New Release'),
},
{
value: ActivationConditionType.DEPLOY_CREATION,
label: t('New Deploy'),
},
]}
required
value={activationCondition}
Expand Down
76 changes: 75 additions & 1 deletion tests/sentry/models/test_releaseprojectenvironment.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
from datetime import timedelta
from unittest.mock import call as mock_call
from unittest.mock import patch

from django.utils import timezone

from sentry.incidents.models.alert_rule import AlertRule, AlertRuleMonitorType
from sentry.incidents.utils.types import AlertRuleActivationConditionType
from sentry.models.environment import Environment
from sentry.models.release import Release
from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment
from sentry.models.releaseprojectenvironment import (
ReleaseProjectEnvironment,
ReleaseProjectEnvironmentManager,
)
from sentry.signals import receivers_raise_on_send
from sentry.snuba.models import QuerySubscription
from sentry.testutils.cases import TestCase


Expand Down Expand Up @@ -83,3 +92,68 @@ def test_no_update_too_close(self):
)
assert release_project_env.first_seen == self.datetime_now
assert release_project_env.last_seen == self.datetime_now

@receivers_raise_on_send()
@patch.object(ReleaseProjectEnvironmentManager, "subscribe_project_to_alert_rule")
def test_post_save_subscribes_project_to_alert_rule_if_created(
self, mock_subscribe_project_to_alert_rule
):
ReleaseProjectEnvironment.get_or_create(
project=self.project,
release=self.release,
environment=self.environment,
datetime=self.datetime_now,
)

assert mock_subscribe_project_to_alert_rule.call_count == 1

@patch(
"sentry.incidents.models.alert_rule.AlertRule.objects.conditionally_subscribe_project_to_alert_rules"
)
def test_subscribe_project_to_alert_rule_constructs_query(self, mock_conditionally_subscribe):
ReleaseProjectEnvironmentManager.subscribe_project_to_alert_rule(
project=self.project, release=self.release, environment=self.environment, trigger="test"
)

assert mock_conditionally_subscribe.call_count == 1
assert mock_conditionally_subscribe.mock_calls == [
mock_call(
project=self.project,
activation_condition=AlertRuleActivationConditionType.DEPLOY_CREATION,
query_extra=f"release:{self.release.version} and environment:{self.environment.name}",
origin="test",
activator=f"release:{self.release.version} and environment:{self.environment.name}",
)
]

def test_unmocked_subscribe_project_to_alert_rule_constructs_query(self):
# Let the logic flow through to snuba and see whether we properly construct the snuba query
# project = self.create_project(name="foo")
# release = Release.objects.create(organization_id=project.organization_id, version="42")
self.create_alert_rule(
projects=[self.project],
monitor_type=AlertRuleMonitorType.ACTIVATED,
activation_condition=AlertRuleActivationConditionType.DEPLOY_CREATION,
)

subscribe_project = AlertRule.objects.conditionally_subscribe_project_to_alert_rules
with patch(
"sentry.incidents.models.alert_rule.AlertRule.objects.conditionally_subscribe_project_to_alert_rules",
wraps=subscribe_project,
) as wrapped_subscribe_project:
with self.tasks():
rpe = ReleaseProjectEnvironmentManager.subscribe_project_to_alert_rule(
project=self.project,
release=self.release,
environment=self.environment,
trigger="test",
)

assert rpe
assert wrapped_subscribe_project.call_count == 1

queryset = QuerySubscription.objects.filter(project=self.project)
assert queryset.exists()

sub = queryset.first()
assert sub.subscription_id is not None

0 comments on commit 6321bd8

Please sign in to comment.