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
5 changes: 5 additions & 0 deletions src/sentry/issues/derived/aggregators.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ def track_views(state: StateView, entry: GroupActionLogEntry) -> AggregatorResul
),
)
def track_status(state: StateView, entry: GroupActionLogEntry) -> AggregatorResult:
# A merge preserves the destination group's status. Ignore actions migrated
# from source groups so their history cannot overwrite that status.
if entry.original_group_id is not None:
return None

current = state[STATUS]

match entry.action:
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/issues/derived/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class IssueStatus(StrEnum):

# Status of the issue based on the log.
STATUS = Feature[IssueStatus](
"status", default=IssueStatus.OPEN, codec=EnumCodec(IssueStatus), version=1
"status", default=IssueStatus.OPEN, codec=EnumCodec(IssueStatus), version=2
)

# The current Progress of the issue.
Expand Down
57 changes: 56 additions & 1 deletion tests/sentry/issues/derived/test_aggregators.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class FakeEntry:
actor_type: int = GroupActorType.SYSTEM
actor_id: int = 0
data: dict[str, object] = field(default_factory=dict)
original_group_id: int | None = None

@property
def action(self) -> GroupAction:
Expand All @@ -84,11 +85,12 @@ def _resolved_pr_data(pr_id: int) -> dict[str, object]:
return {"pull_request": pr_id}


def _reconcile_entry(status: IssueStatus) -> FakeEntry:
def _reconcile_entry(status: IssueStatus, *, original_group_id: int | None = None) -> FakeEntry:
action = ReconcileStatusAction(status=status.value)
return FakeEntry(
type=GroupActionType.RECONCILE_STATUS,
data=action.dict(),
original_group_id=original_group_id,
)


Expand Down Expand Up @@ -168,6 +170,26 @@ def test_close_actions(action_type: GroupActionType) -> None:
)


@pytest.mark.parametrize(
"action_type",
[
GroupActionType.RESOLVE,
GroupActionType.SET_RESOLVED_IN_RELEASE,
GroupActionType.SET_RESOLVED_BY_AGE,
GroupActionType.SET_RESOLVED_IN_COMMIT,
GroupActionType.ARCHIVE,
],
)
def test_close_actions_from_merged_groups_are_ignored(action_type: GroupActionType) -> None:
assert (
_run_for_feature(
STATUS,
[FakeEntry(type=action_type, original_group_id=123)],
)
== IssueStatus.OPEN
)


def test_unresolve_reopens() -> None:
assert (
_run_for_feature(
Expand All @@ -181,6 +203,27 @@ def test_unresolve_reopens() -> None:
)


@pytest.mark.parametrize(
"action_type",
[
GroupActionType.UNRESOLVE,
GroupActionType.SET_REGRESSED,
GroupActionType.SET_ESCALATING,
],
)
def test_reopen_actions_from_merged_groups_are_ignored(action_type: GroupActionType) -> None:
assert (
_run_for_feature(
STATUS,
[
FakeEntry(type=GroupActionType.RESOLVE),
FakeEntry(type=action_type, original_group_id=123),
],
)
== IssueStatus.CLOSED
)


def test_duplicate_resolve_is_noop() -> None:
assert (
_run_for_feature(
Expand Down Expand Up @@ -310,6 +353,18 @@ def test_reopens_closed(self) -> None:
== IssueStatus.OPEN
)

def test_from_merged_group_is_ignored(self) -> None:
assert (
_run_for_feature(
STATUS,
[
FakeEntry(type=GroupActionType.RESOLVE),
_reconcile_entry(IssueStatus.OPEN, original_group_id=123),
],
)
== IssueStatus.CLOSED
)

def test_same_value_is_noop(self) -> None:
assert (
_run_for_feature(
Expand Down
Loading