Skip to content

Commit

Permalink
add TaggedEventConditionHandler
Browse files Browse the repository at this point in the history
  • Loading branch information
ameliahsu committed Jan 3, 2025
1 parent 36db4b1 commit d67aa9f
Show file tree
Hide file tree
Showing 5 changed files with 295 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 @@ -9,6 +9,7 @@
"FirstSeenEventConditionHandler",
"NewHighPriorityIssueConditionHandler",
"LevelConditionHandler",
"TaggedEventConditionHandler",
]

from .group_event_handlers import (
Expand All @@ -17,6 +18,7 @@
EventSeenCountConditionHandler,
EveryEventConditionHandler,
LevelConditionHandler,
TaggedEventConditionHandler,
)
from .group_state_handlers import (
ExistingHighPriorityIssueConditionHandler,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import sentry_sdk

from sentry import tagstore
from sentry.constants import LOG_LEVELS_MAP
from sentry.eventstore.models import GroupEvent
from sentry.rules import MatchType, match_values
Expand Down Expand Up @@ -116,3 +117,50 @@ def evaluate_value(job: WorkflowJob, comparison: Any) -> bool:
elif desired_match == MatchType.LESS_OR_EQUAL:
return level <= desired_level
return False


@condition_handler_registry.register(Condition.TAGGED_EVENT)
class TaggedEventConditionHandler(DataConditionHandler[WorkflowJob]):
@staticmethod
def evaluate_value(job: WorkflowJob, comparison: Any) -> bool:
event = job["event"]
raw_tags = event.tags
key = comparison.get("key")
match = comparison.get("match")
value = comparison.get("value")

if not key or not match:
return False

key = key.lower()

tag_keys = (
k
for gen in (
(k.lower() for k, v in raw_tags),
(tagstore.backend.get_standardized_key(k) for k, v in raw_tags),
)
for k in gen
)

# NOTE: IS_SET condition differs btw tagged_event and event_attribute so not handled by match_values
if match == MatchType.IS_SET:
return key in tag_keys

elif match == MatchType.NOT_SET:
return key not in tag_keys

if not value:
return False

value = value.lower()

# This represents the fetched tag values given the provided key
# so eg. if the key is 'environment' and the tag_value is 'production'
tag_values = (
v.lower()
for k, v in raw_tags
if k.lower() == key or tagstore.backend.get_standardized_key(k) == key
)

return match_values(group_values=tag_values, match_value=value, match_type=match)
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
from sentry.rules.conditions.new_high_priority_issue import NewHighPriorityIssueCondition
from sentry.rules.conditions.reappeared_event import ReappearedEventCondition
from sentry.rules.conditions.regression_event import RegressionEventCondition
from sentry.rules.conditions.tagged_event import TaggedEventCondition
from sentry.rules.filters.event_attribute import EventAttributeFilter
from sentry.rules.filters.level import LevelFilter
from sentry.rules.filters.tagged_event import TaggedEventFilter
from sentry.rules.match import MatchType
from sentry.utils.registry import Registry
from sentry.workflow_engine.models.data_condition import Condition, DataCondition
from sentry.workflow_engine.models.data_condition_group import DataConditionGroup
Expand Down Expand Up @@ -129,3 +132,24 @@ def create_level_condition(data: dict[str, Any], dcg: DataConditionGroup) -> Dat
condition_result=True,
condition_group=dcg,
)


@data_condition_translator_registry.register(TaggedEventCondition.id)
@data_condition_translator_registry.register(TaggedEventFilter.id)
def create_tagged_event_data_condition(
data: dict[str, Any], dcg: DataConditionGroup
) -> DataCondition:
# TODO: Add comparison validation (error if not enough information)
comparison = {
"match": data["match"],
"key": data["key"],
}
if comparison["match"] not in {MatchType.IS_SET, MatchType.NOT_SET}:
comparison["value"] = data["value"]

return DataCondition.objects.create(
type=Condition.TAGGED_EVENT,
comparison=comparison,
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 @@ -30,6 +30,7 @@ class Condition(models.TextChoices):
NEW_HIGH_PRIORITY_ISSUE = "new_high_priority_issue"
REGRESSION_EVENT = "regression_event"
REAPPEARED_EVENT = "reappeared_event"
TAGGED_EVENT = "tagged_event"


condition_ops = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
from sentry.rules.conditions.tagged_event import TaggedEventCondition
from sentry.rules.match import MatchType
from sentry.workflow_engine.models.data_condition import Condition
from sentry.workflow_engine.types import WorkflowJob
from tests.sentry.workflow_engine.handlers.condition.test_base import ConditionTestCase


class TestTaggedEventCondition(ConditionTestCase):
condition = Condition.TAGGED_EVENT
rule_cls = TaggedEventCondition
payload = {
"id": TaggedEventCondition.id,
"match": MatchType.EQUAL,
"key": "LOGGER",
"value": "sentry.example",
}

def get_event(self):
event = self.event
event.data["tags"] = (
("logger", "sentry.example"),
("logger", "foo.bar"),
("notlogger", "sentry.other.example"),
("notlogger", "bar.foo.baz"),
)
return event

def setUp(self):
super().setUp()
self.event = self.get_event()
self.group = self.create_group(project=self.project)
self.group_event = self.event.for_group(self.group)
self.job = WorkflowJob(
{
"event": self.group_event,
"has_reappeared": True,
}
)
self.dc = self.create_data_condition(
type=self.condition,
comparison={"match": MatchType.EQUAL, "key": "LOGGER", "value": "sentry.example"},
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 == {
"match": MatchType.EQUAL,
"key": "LOGGER",
"value": "sentry.example",
}
assert dc.condition_result is True
assert dc.condition_group == dcg

self.payload = {
"id": TaggedEventCondition.id,
"match": MatchType.IS_SET,
"key": "logger",
}
dcg = self.create_data_condition_group()
dc = self.translate_to_data_condition(self.payload, dcg)

assert dc.type == self.condition
assert dc.comparison == {
"match": MatchType.IS_SET,
"key": "logger",
}
assert dc.condition_result is True
assert dc.condition_group == dcg

def test_equals(self):
self.dc.comparison = {"match": MatchType.EQUAL, "key": "LOGGER", "value": "sentry.example"}
self.assert_passes(self.dc, self.job)

self.dc.comparison = {
"match": MatchType.EQUAL,
"key": "logger",
"value": "sentry.other.example",
}
self.assert_does_not_pass(self.dc, self.job)

def test_does_not_equal(self):
self.dc.comparison = {
"match": MatchType.NOT_EQUAL,
"key": "logger",
"value": "sentry.example",
}
self.assert_does_not_pass(self.dc, self.job)

self.dc.comparison = {
"match": MatchType.NOT_EQUAL,
"key": "logger",
"value": "sentry.other.example",
}
self.assert_passes(self.dc, self.job)

def test_starts_with(self):
self.dc.comparison = {
"match": MatchType.STARTS_WITH,
"key": "logger",
"value": "sentry.",
}
self.assert_passes(self.dc, self.job)

self.dc.comparison = {"match": MatchType.STARTS_WITH, "key": "logger", "value": "bar."}
self.assert_does_not_pass(self.dc, self.job)

def test_does_not_start_with(self):
self.dc.comparison = {
"match": MatchType.NOT_STARTS_WITH,
"key": "logger",
"value": "sentry.",
}
self.assert_does_not_pass(self.dc, self.job)

self.dc.comparison = {
"match": MatchType.NOT_STARTS_WITH,
"key": "logger",
"value": "bar.",
}
self.assert_passes(self.dc, self.job)

def test_ends_with(self):
self.dc.comparison = {
"match": MatchType.ENDS_WITH,
"key": "logger",
"value": ".example",
}
self.assert_passes(self.dc, self.job)

self.dc.comparison = {"match": MatchType.ENDS_WITH, "key": "logger", "value": ".foo"}
self.assert_does_not_pass(self.dc, self.job)

def test_does_not_end_with(self):
self.dc.comparison = {
"match": MatchType.NOT_ENDS_WITH,
"key": "logger",
"value": ".example",
}
self.assert_does_not_pass(self.dc, self.job)

self.dc.comparison = {
"match": MatchType.NOT_ENDS_WITH,
"key": "logger",
"value": ".foo",
}
self.assert_passes(self.dc, self.job)

def test_contains(self):
self.dc.comparison = {
"match": MatchType.CONTAINS,
"key": "logger",
"value": "sentry",
}
self.assert_passes(self.dc, self.job)

self.dc.comparison = {"match": MatchType.CONTAINS, "key": "logger", "value": "bar.foo"}
self.assert_does_not_pass(self.dc, self.job)

def test_does_not_contain(self):
self.dc.comparison = {
"match": MatchType.NOT_CONTAINS,
"key": "logger",
"value": "sentry",
}
self.assert_does_not_pass(self.dc, self.job)

self.dc.comparison = {
"match": MatchType.NOT_CONTAINS,
"key": "logger",
"value": "bar.foo",
}
self.assert_passes(self.dc, self.job)

def test_is_set(self):
self.dc.comparison = {"match": MatchType.IS_SET, "key": "logger"}
self.assert_passes(self.dc, self.job)

self.dc.comparison = {"match": MatchType.IS_SET, "key": "missing"}
self.assert_does_not_pass(self.dc, self.job)

def test_is_not_set(self):
self.dc.comparison = {"match": MatchType.NOT_SET, "key": "logger"}
self.assert_does_not_pass(self.dc, self.job)

self.dc.comparison = {"match": MatchType.NOT_SET, "key": "missing"}
self.assert_passes(self.dc, self.job)

def test_is_in(self):
self.dc.comparison = {
"match": MatchType.IS_IN,
"key": "logger",
"value": "bar.foo, wee, wow",
}
self.assert_does_not_pass(self.dc, self.job)

self.dc.comparison = {
"match": MatchType.IS_IN,
"key": "logger",
"value": "foo.bar",
}
self.assert_passes(self.dc, self.job)

def test_not_in(self):
self.dc.comparison = {
"match": MatchType.NOT_IN,
"key": "logger",
"value": "bar.foo, wee, wow",
}
self.assert_passes(self.dc, self.job)

self.dc.comparison = {
"match": MatchType.NOT_IN,
"key": "logger",
"value": "foo.bar",
}
self.assert_does_not_pass(self.dc, self.job)

0 comments on commit d67aa9f

Please sign in to comment.