Skip to content

Commit

Permalink
add LatestReleaseConditionHandler
Browse files Browse the repository at this point in the history
  • Loading branch information
ameliahsu committed Jan 8, 2025
1 parent a358b84 commit c2073ae
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/sentry/workflow_engine/handlers/condition/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"TaggedEventConditionHandler",
"AgeComparisonConditionHandler",
"AssignedToConditionHandler",
"LatestReleaseConditionHandler",
]

from .age_comparison_handler import AgeComparisonConditionHandler
Expand All @@ -22,6 +23,7 @@
from .every_event_handler import EveryEventConditionHandler
from .existing_high_priority_issue_handler import ExistingHighPriorityIssueConditionHandler
from .first_seen_event_handler import FirstSeenEventConditionHandler
from .latest_release_handler import LatestReleaseConditionHandler
from .level_handler import LevelConditionHandler
from .new_high_priority_issue_handler import NewHighPriorityIssueConditionHandler
from .reappeared_event_handler import ReappearedEventConditionHandler
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from typing import Any

from sentry import tagstore
from sentry.eventstore.models import GroupEvent
from sentry.models.environment import Environment
from sentry.models.release import Release
from sentry.rules.filters.latest_release import get_project_release_cache_key
from sentry.search.utils import get_latest_release
from sentry.utils.cache import cache
from sentry.workflow_engine.models.data_condition import Condition
from sentry.workflow_engine.registry import condition_handler_registry
from sentry.workflow_engine.types import DataConditionHandler, WorkflowJob


@condition_handler_registry.register(Condition.LATEST_RELEASE)
class LatestReleaseConditionHandler(DataConditionHandler[WorkflowJob]):
@staticmethod
def get_latest_release(environment_id: int | None, event: GroupEvent) -> Release | None:
cache_key = get_project_release_cache_key(event.group.project_id, environment_id)
cached_latest_release = cache.get(cache_key)
if cached_latest_release is None:
organization_id = event.group.project.organization_id
environments = None
if environment_id:
environments = [Environment.objects.get(id=environment_id)]
try:
latest_release_version = get_latest_release(
[event.group.project],
environments,
organization_id,
)[0]
except Release.DoesNotExist:
return None
latest_release = Release.objects.filter(
version=latest_release_version, organization_id=organization_id
).first()
if latest_release:
cache.set(cache_key, latest_release, 600)
return latest_release
else:
cache.set(cache_key, False, 600)
return cached_latest_release

@staticmethod
def evaluate_value(job: WorkflowJob, comparison: Any) -> bool:
event = job["event"]
workflow = job.get("workflow")
environment_id = workflow.environment_id if workflow else None
latest_release = LatestReleaseConditionHandler.get_latest_release(environment_id, event)
if not latest_release:
return False

releases = (
v.lower()
for k, v in event.tags
if k.lower() == "release" or tagstore.backend.get_standardized_key(k) == "release"
)

return any(release == latest_release.version.lower() for release in releases)
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from sentry.rules.filters.age_comparison import AgeComparisonFilter
from sentry.rules.filters.assigned_to import AssignedToFilter
from sentry.rules.filters.event_attribute import EventAttributeFilter
from sentry.rules.filters.latest_release import LatestReleaseFilter
from sentry.rules.filters.level import LevelFilter
from sentry.rules.filters.tagged_event import TaggedEventFilter
from sentry.rules.match import MatchType
Expand Down Expand Up @@ -192,3 +193,15 @@ def create_assigned_to_data_condition(
condition_result=True,
condition_group=dcg,
)


@data_condition_translator_registry.register(LatestReleaseFilter.id)
def create_latest_release_data_condition(
data: dict[str, Any], dcg: DataConditionGroup
) -> DataCondition:
return DataCondition.objects.create(
type=Condition.LATEST_RELEASE,
comparison=True,
condition_result=True,
condition_group=dcg,
)
1 change: 1 addition & 0 deletions src/sentry/workflow_engine/models/data_condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Condition(models.TextChoices):
EVERY_EVENT = "every_event"
EXISTING_HIGH_PRIORITY_ISSUE = "existing_high_priority_issue"
FIRST_SEEN_EVENT = "first_seen_event"
LATEST_RELEASE = "latest_release"
LEVEL = "level"
NEW_HIGH_PRIORITY_ISSUE = "new_high_priority_issue"
REGRESSION_EVENT = "regression_event"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
from datetime import UTC, datetime

import pytest

from sentry.models.release import Release
from sentry.rules.filters.latest_release import LatestReleaseFilter, get_project_release_cache_key
from sentry.testutils.skips import requires_snuba
from sentry.utils.cache import cache
from sentry.workflow_engine.models.data_condition import Condition
from sentry.workflow_engine.models.workflow import Workflow
from sentry.workflow_engine.types import WorkflowJob
from tests.sentry.workflow_engine.handlers.condition.test_base import ConditionTestCase

pytestmark = [requires_snuba, pytest.mark.sentry_metrics]


class TestLatestReleaseCondition(ConditionTestCase):
condition = Condition.LATEST_RELEASE
rule_cls = LatestReleaseFilter
payload = {
"id": LatestReleaseFilter.id,
}

def setup_group_event_and_job(self):
self.group_event = self.event.for_group(self.group)
self.job = WorkflowJob(
{
"event": self.group_event,
"workflow": Workflow(environment_id=None),
}
)

def setUp(self):
super().setUp()
self.job = WorkflowJob(
{
"event": self.group_event,
}
)
self.dc = self.create_data_condition(
type=self.condition,
comparison=True,
condition_result=True,
)

def test_dual_write(self):
dcg = self.create_data_condition_group()
dc = self.translate_to_data_condition(self.payload, dcg)

assert dc.type == self.condition
assert dc.comparison is True
assert dc.condition_result is True
assert dc.condition_group == dcg

def test_latest_release(self):
oldRelease = Release.objects.create(
organization_id=self.organization.id,
version="1",
date_added=datetime(2020, 9, 1, 3, 8, 24, 880386, tzinfo=UTC),
)
oldRelease.add_project(self.project)

newRelease = Release.objects.create(
organization_id=self.organization.id,
version="2",
date_added=datetime(2020, 9, 2, 3, 8, 24, 880386, tzinfo=UTC),
)
newRelease.add_project(self.project)

self.event.data["tags"] = (("release", newRelease.version),)
self.assert_passes(self.dc, self.job)

def test_latest_release_no_match(self):
oldRelease = Release.objects.create(
organization_id=self.organization.id,
version="1",
date_added=datetime(2020, 9, 1, 3, 8, 24, 880386, tzinfo=UTC),
)
oldRelease.add_project(self.project)

newRelease = Release.objects.create(
organization_id=self.organization.id,
version="2",
date_added=datetime(2020, 9, 2, 3, 8, 24, 880386, tzinfo=UTC),
)
newRelease.add_project(self.project)

self.event.data["tags"] = (("release", oldRelease.version),)
self.assert_does_not_pass(self.dc, self.job)

def test_caching(self):
oldRelease = Release.objects.create(
organization_id=self.organization.id,
version="1",
date_added=datetime(2020, 9, 1, 3, 8, 24, 880386, tzinfo=UTC),
)
oldRelease.add_project(self.project)
self.event.data["tags"] = (("release", oldRelease.version),)
self.assert_passes(self.dc, self.job)

newRelease = Release.objects.create(
organization_id=self.organization.id,
version="2",
date_added=datetime(2020, 9, 2, 3, 8, 24, 880386, tzinfo=UTC),
)
newRelease.add_project(self.project)

# ensure we clear the cache after creating a new release
cache_key = get_project_release_cache_key(self.event.group.project_id)
assert cache.get(cache_key) is None

self.assert_does_not_pass(self.dc, self.job)

# ensure we clear the cache when a release is deleted
newRelease.safe_delete()
cache_key = get_project_release_cache_key(self.event.group.project_id)
assert cache.get(cache_key) is None

# rule should pass again because the latest release is oldRelease
self.assert_passes(self.dc, self.job)

def test_latest_release_with_environment(self):
self.create_release(
project=self.event.group.project,
version="1",
date_added=datetime(2020, 9, 1, 3, 8, 24, 880386, tzinfo=UTC),
environments=[self.environment],
)

new_release = self.create_release(
project=self.event.group.project,
version="2",
date_added=datetime(2020, 9, 2, 3, 8, 24, 880386, tzinfo=UTC),
environments=[self.environment],
)

other_env_release = self.create_release(
project=self.event.group.project,
version="4",
date_added=datetime(2020, 9, 3, 3, 8, 24, 880386, tzinfo=UTC),
)

self.job = WorkflowJob(
{
"event": self.group_event,
"workflow": Workflow(environment_id=self.environment.id),
}
)

self.event.data["tags"] = (("release", new_release.version),)
self.assert_passes(self.dc, self.job)

self.event.data["tags"] = (("release", other_env_release.version),)
self.assert_does_not_pass(self.dc, self.job)

0 comments on commit c2073ae

Please sign in to comment.