Skip to content

feat(issue-platform): Allow assignee to be passed with the occurrence #69031

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 16, 2024
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
24 changes: 24 additions & 0 deletions src/sentry/issues/issue_occurrence.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import hashlib
import logging
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from datetime import datetime
Expand All @@ -10,6 +11,7 @@

from sentry import nodestore
from sentry.issues.grouptype import GroupType, get_group_type_by_type_id
from sentry.utils.actor import ActorTuple
from sentry.utils.dates import parse_timestamp

DEFAULT_LEVEL = "info"
Expand All @@ -36,6 +38,11 @@ class IssueOccurrenceData(TypedDict):
level: str | None
culprit: str | None
initial_issue_priority: NotRequired[int | None]
assignee: NotRequired[str | None]
"""
Who to assign the issue to when creating a new issue. Has no effect on existing issues.
In the format of an Actor identifier, as defined in `ActorTuple.from_actor_identifier`
"""


@dataclass(frozen=True)
Expand Down Expand Up @@ -88,6 +95,7 @@ class IssueOccurrence:
level: str
culprit: str
initial_issue_priority: int | None = None
assignee: ActorTuple | None = None

def __post_init__(self) -> None:
if not is_aware(self.detection_time):
Expand All @@ -111,17 +119,32 @@ def to_dict(
"level": self.level,
"culprit": self.culprit,
"initial_issue_priority": self.initial_issue_priority,
"assignee": self.assignee.identifier if self.assignee else None,
}

@classmethod
def from_dict(cls, data: IssueOccurrenceData) -> IssueOccurrence:
from sentry.api.serializers.rest_framework import ValidationError

# Backwards compatibility - we used to not require this field, so set a default when `None`
level = data.get("level")
if not level:
level = DEFAULT_LEVEL
culprit = data.get("culprit")
if not culprit:
culprit = ""

assignee = None
try:
# Note that this can cause IO, but in practice this will happen only the first time that
# the occurrence is sent to the issue platform. We then translate to the id and store
# that, so subsequent fetches won't cause IO.
assignee = ActorTuple.from_actor_identifier(data.get("assignee"))
except ValidationError:
logging.exception("Failed to parse assignee actor identifier")
except Exception:
# We never want this to cause parsing an occurrence to fail
logging.exception("Unexpected error parsing assignee")
return cls(
data["id"],
data["project_id"],
Expand All @@ -141,6 +164,7 @@ def from_dict(cls, data: IssueOccurrenceData) -> IssueOccurrence:
level,
culprit,
data.get("initial_issue_priority"),
assignee,
)

@property
Expand Down
1 change: 1 addition & 0 deletions src/sentry/issues/occurrence_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ def _get_kwargs(payload: Mapping[str, Any]) -> Mapping[str, Any]:
"type": payload["type"],
"detection_time": payload["detection_time"],
"level": payload.get("level", DEFAULT_LEVEL),
"assignee": payload.get("assignee"),
}

process_occurrence_data(occurrence_data)
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/utils/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def from_actor_identifier(cls, actor_identifier: int | str | None) -> ActorTuple
"maisey@dogsrule.com" -> look up User by primary email
"""

if actor_identifier is None:
if not actor_identifier:
return None

# If we have an integer, fall back to assuming it's a User
Expand Down
32 changes: 32 additions & 0 deletions tests/sentry/issues/test_issue_occurrence.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from sentry.issues.issue_occurrence import DEFAULT_LEVEL, IssueEvidence, IssueOccurrence
from sentry.models.team import Team
from sentry.models.user import User
from sentry.testutils.cases import TestCase
from sentry.utils.actor import ActorTuple
from tests.sentry.issues.test_utils import OccurrenceTestMixin


Expand All @@ -16,6 +19,35 @@ def test_level_default(self) -> None:
occurrence = IssueOccurrence.from_dict(occurrence_data)
assert occurrence.level == DEFAULT_LEVEL

def test_assignee(self) -> None:
occurrence_data = self.build_occurrence_data()
occurrence_data["assignee"] = f"user:{self.user.id}"
occurrence = IssueOccurrence.from_dict(occurrence_data)
assert occurrence.assignee == ActorTuple(self.user.id, User)
occurrence_data["assignee"] = f"{self.user.id}"
occurrence = IssueOccurrence.from_dict(occurrence_data)
assert occurrence.assignee == ActorTuple(self.user.id, User)
occurrence_data["assignee"] = f"{self.user.email}"
occurrence = IssueOccurrence.from_dict(occurrence_data)
assert occurrence.assignee == ActorTuple(self.user.id, User)
occurrence_data["assignee"] = f"{self.user.username}"
occurrence = IssueOccurrence.from_dict(occurrence_data)
assert occurrence.assignee == ActorTuple(self.user.id, User)
occurrence_data["assignee"] = f"team:{self.team.id}"
occurrence = IssueOccurrence.from_dict(occurrence_data)
assert occurrence.assignee == ActorTuple(self.team.id, Team)

def test_assignee_none(self) -> None:
occurrence_data = self.build_occurrence_data()
occurrence = IssueOccurrence.from_dict(occurrence_data)
assert occurrence.assignee is None
occurrence_data["assignee"] = None
occurrence = IssueOccurrence.from_dict(occurrence_data)
assert occurrence.assignee is None
occurrence_data["assignee"] = ""
occurrence = IssueOccurrence.from_dict(occurrence_data)
assert occurrence.assignee is None


class IssueOccurrenceSaveAndFetchTest(OccurrenceTestMixin, TestCase):
def test(self) -> None:
Expand Down
18 changes: 18 additions & 0 deletions tests/sentry/issues/test_occurrence_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,3 +458,21 @@ def test_priority_overrides_defaults(self) -> None:
message["initial_issue_priority"] = PriorityLevel.HIGH
kwargs = _get_kwargs(message)
assert kwargs["occurrence_data"]["initial_issue_priority"] == PriorityLevel.HIGH

def test_assignee(self) -> None:
message = deepcopy(get_test_message(self.project.id))
message["assignee"] = f"user:{self.user.id}"
kwargs = _get_kwargs(message)
assert kwargs["occurrence_data"]["assignee"] == f"user:{self.user.id}"

def test_assignee_none(self) -> None:
kwargs = _get_kwargs(deepcopy(get_test_message(self.project.id)))
assert kwargs["occurrence_data"]["assignee"] is None
message = deepcopy(get_test_message(self.project.id))
message["assignee"] = None
kwargs = _get_kwargs(message)
assert kwargs["occurrence_data"]["assignee"] is None
message = deepcopy(get_test_message(self.project.id))
message["assignee"] = ""
kwargs = _get_kwargs(message)
assert kwargs["occurrence_data"]["assignee"] == ""
Loading