Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions src/sentry/grouping/grouptype.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
from dataclasses import dataclass
from typing import TypeVar

from sentry.issues.grouptype import GroupCategory, GroupType
from sentry.models.group import DEFAULT_TYPE_ID
from sentry.types.group import PriorityLevel
from sentry.workflow_engine.endpoints.validators.error_detector import ErrorDetectorValidator
from sentry.workflow_engine.handlers.detector.base import (
DetectorHandler,
GroupedDetectorEvaluationResult,
)
from sentry.workflow_engine.handlers.detector.base import DetectorHandler
from sentry.workflow_engine.models.data_source import DataPacket
from sentry.workflow_engine.types import DetectorSettings
from sentry.workflow_engine.types import (
DetectorEvaluationResult,
DetectorGroupKey,
DetectorSettings,
)

T = TypeVar("T")

class ErrorDetectorHandler(DetectorHandler[object]):
"""Placeholder handler for error group types."""

class ErrorDetectorHandler(DetectorHandler):
def evaluate_impl(self, data_packet: DataPacket[T]) -> GroupedDetectorEvaluationResult:
# placeholder
return GroupedDetectorEvaluationResult(result={}, tainted=False)
def evaluate(
self, data_packet: DataPacket[object]
) -> dict[DetectorGroupKey, DetectorEvaluationResult]:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing i've struggled with in these abstractions is the DetectorGroupKey - and grouping in general for the results.. 🤔 should we have a grouping detector that handles all of this?

I haven't bottomed out on any of that stuff, just a thought i've been struggling with when looking at the base class. (the functional programmer in me wants to make GroupingDetector and decorate these classes with it)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been spinning on this a bit myself, and I'm not sure yet. This change is partially a spinoff of me trying to reason through these issues.

return {}


@dataclass(frozen=True)
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/workflow_engine/handlers/detector/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
__all__ = [
"BaseDetectorHandler",
"DataPacketEvaluationType",
"DataPacketType",
"DetectorHandler",
Expand All @@ -9,6 +10,7 @@
]

from .base import (
BaseDetectorHandler,
DataPacketEvaluationType,
DataPacketType,
DetectorHandler,
Expand Down
32 changes: 29 additions & 3 deletions src/sentry/workflow_engine/handlers/detector/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,37 @@ class GroupedDetectorEvaluationResult:
tainted: bool


# TODO - @saponifi3d - Change this class to be a pure ABC and remove the `__init__` method.
# TODO - @saponifi3d - Once the change is made, we should introduce a `BaseDetector` class to evaluate simple cases
class DetectorHandler(abc.ABC, Generic[DataPacketType, DataPacketEvaluationType]):
class DetectorHandler(abc.ABC, Generic[DataPacketType]):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is kept as a pure ABC, should it live in workflow_engine/types?

"""
Abstract base class defining the public interface for detector handlers.
"""

def __init__(self, detector: Detector):
self.detector = detector
Comment on lines 94 to 95
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep this as a pure abc and only have type definitions?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to, but an abstract constructor felt strange when an actual constructor gave us what we needed. DetectorSettings could use a Callable which constructs the correct type, could've gone with an abstract static constructor, but in nearly all worlds it seemed like we'd just be requiring someone to implement this init, so I went with it. Open to alternatives, but wasn't sold by the ones I considered as being better.


@abc.abstractmethod
def evaluate(
self, data_packet: DataPacket[DataPacketType]
) -> dict[DetectorGroupKey, DetectorEvaluationResult]:
pass


class BaseDetectorHandler(
DetectorHandler[DataPacketType],
Generic[DataPacketType, DataPacketEvaluationType],
):
"""
Base implementation class providing shared infrastructure for detector handlers.
Includes metrics tracking, condition group loading, and defines abstract methods
that concrete handlers must implement.

DataPacketType is what we've embedded within the data packet.
DataPacketEvaluationType is the type of the value to be extracted from the data packet and
used to evaluate the conditions on the detector.
"""

def __init__(self, detector: Detector):
super().__init__(detector)
if detector.workflow_condition_group_id is not None:
try:
# Check if workflow_condition_group is already prefetched
Expand Down
7 changes: 3 additions & 4 deletions src/sentry/workflow_engine/handlers/detector/stateful.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import dataclasses
import logging
from datetime import timedelta
from typing import Any, Generic, cast
from typing import Any, cast
from uuid import uuid4

from django.conf import settings
Expand All @@ -16,9 +16,9 @@
from sentry.models.group import GroupStatus
from sentry.utils import metrics, redis
from sentry.workflow_engine.handlers.detector.base import (
BaseDetectorHandler,
DataPacketEvaluationType,
DataPacketType,
DetectorHandler,
DetectorOccurrence,
EventData,
GroupedDetectorEvaluationResult,
Expand Down Expand Up @@ -309,8 +309,7 @@ def get_state_data(


class StatefulDetectorHandler(
Generic[DataPacketType, DataPacketEvaluationType],
DetectorHandler[DataPacketType, DataPacketEvaluationType],
BaseDetectorHandler[DataPacketType, DataPacketEvaluationType],
abc.ABC,
):
"""
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/workflow_engine/models/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def group_type(self) -> builtins.type[GroupType]:
return group_type

@property
def detector_handler(self) -> DetectorHandler[Any, Any] | None:
def detector_handler(self) -> DetectorHandler[Any] | None:
group_type = self.group_type

if self.settings.handler is None:
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/workflow_engine/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ class SnubaQueryDataSourceType(TypedDict):

@dataclass(frozen=True)
class DetectorSettings:
handler: type[DetectorHandler[Any, Any]] | None = None
handler: type[DetectorHandler[Any]] | None = None
validator: type[BaseDetectorTypeValidator] | None = None
config_schema: dict[str, Any] = field(default_factory=dict)
filter: Q | None = None
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from sentry.testutils.silo import region_silo_test
from sentry.uptime.grouptype import UptimeDomainCheckFailure
from sentry.workflow_engine.handlers.detector import (
DetectorHandler,
BaseDetectorHandler,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for future - this could be a super cool abstraction; we could create a TestDetectorHandler and all the registration info. right now most of the tests are loosely based on an existing detector type cause... time.

DetectorOccurrence,
GroupedDetectorEvaluationResult,
)
Expand Down Expand Up @@ -42,7 +42,7 @@ def setUp(self) -> None:
)
self.registry_patcher.start()

class MockDetectorHandler(DetectorHandler[dict[Never, Never], bool]):
class MockDetectorHandler(BaseDetectorHandler[dict[Never, Never], bool]):
def evaluate_impl(
self, data_packet: DataPacket[dict[Never, Never]]
) -> GroupedDetectorEvaluationResult:
Expand Down
7 changes: 4 additions & 3 deletions tests/sentry/workflow_engine/handlers/detector/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from sentry.testutils.abstract import Abstract
from sentry.types.group import PriorityLevel
from sentry.workflow_engine.handlers.detector import (
BaseDetectorHandler,
DataPacketEvaluationType,
DetectorHandler,
DetectorOccurrence,
Expand All @@ -27,7 +28,7 @@


def build_mock_occurrence_and_event(
handler: DetectorHandler[Any, Any],
handler: DetectorHandler[Any],
value: DataPacketEvaluationType,
priority: PriorityLevel,
) -> tuple[DetectorOccurrence, dict[str, Any]]:
Expand Down Expand Up @@ -99,7 +100,7 @@ class NoHandlerGroupType(GroupType):
category = GroupCategory.METRIC_ALERT.value
category_v2 = GroupCategory.METRIC_ALERT.value

class MockDetectorHandler(DetectorHandler[dict[str, Any], int]):
class MockDetectorHandler(BaseDetectorHandler[dict[str, Any], int]):
def evaluate_impl(
self, data_packet: DataPacket[dict[str, Any]]
) -> GroupedDetectorEvaluationResult:
Expand All @@ -123,7 +124,7 @@ def create_occurrence(
def extract_dedupe_value(self, data_packet: DataPacket[dict[str, Any]]) -> int:
return data_packet.packet.get("dedupe", 0)

class MockDetectorWithUpdateHandler(DetectorHandler[dict[str, Any], int]):
class MockDetectorWithUpdateHandler(BaseDetectorHandler[dict[str, Any], int]):
def evaluate_impl(
self, data_packet: DataPacket[dict[str, Any]]
) -> GroupedDetectorEvaluationResult:
Expand Down
Loading