Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
10 changes: 8 additions & 2 deletions src/sentry/tasks/groupowner.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,17 @@ def _process_suspect_commits(
project, group_id, event_frames, event_platform, sdk_name=sdk_name
)
owner_scores: dict[str, int] = {}
owner_commits: dict[str, int] = {}
for committer in committers:
author = cast(UserSerializerResponse, committer["author"])
if author and "id" in author:
author_id = author["id"]
for _, score in committer["commits"]:
for commit, score in committer["commits"]:
if score >= MIN_COMMIT_SCORE:
owner_scores[author_id] = max(score, owner_scores.get(author_id, 0))
current_score = owner_scores.get(author_id, 0)
if score > current_score:
owner_scores[author_id] = score
owner_commits[author_id] = commit.id

if owner_scores:
for owner_id, _ in sorted(
Expand All @@ -99,11 +103,13 @@ def _process_suspect_commits(
"user_id": owner_id,
"project_id": project.id,
"organization_id": project.organization_id,
"context__asjsonb__commitId": owner_commits[owner_id],
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: Owner Duplication: Lookup Prevents Record Updates

Including commitId in lookup_kwargs prevents updating existing GroupOwner records when the same author has a new suspect commit. Instead of updating the existing owner's commitId, a new GroupOwner is created, causing duplicate owners for the same user/group combination and breaking the PREFERRED_GROUP_OWNERS limit enforcement. The lookup should match on (group_id, type, user_id, project_id, organization_id) to update existing records, not create duplicates.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

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

I want separate GroupOwners for separate commits. This record does not represent a Group + User, it represents a Group + Commit

Copy link
Member

Choose a reason for hiding this comment

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

do we have any tests which will test that this task will successfully update a group owner with the right fields?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes - take a look at test_update_existing_entries https://github.com/getsentry/sentry/pull/103006/files#diff-b6e0afbaeb8ea99203894d42f9693cc2acc50110771a7ded33a4115cd26c62f0R272
Just added a better check for commitId
This shows we would not create duplicates but LMK if there are other test cases I should cover 👍

},
defaults={
"date_added": timezone.now(),
},
context_defaults={
"commitId": owner_commits[owner_id],
"suspectCommitStrategy": SuspectCommitStrategy.RELEASE_BASED,
},
)
Expand Down
48 changes: 48 additions & 0 deletions tests/sentry/api/endpoints/test_event_committers.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,51 @@ def test_endpoint_with_no_user_groupowner(self) -> None:
)
assert "group_owner_id" in response.data["committers"][0]
assert response.data["committers"][0]["group_owner_id"] == group_owner.id

def test_release_based_suspect_commit_displayed(self) -> None:
"""Test that RELEASE_BASED suspect commits are displayed via the endpoint."""
self.login_as(user=self.user)
project = self.create_project()

repo = self.create_repo(project=project, name="example/repo")
release = self.create_release(project=project, version="v1.0")
commit = self.create_commit(project=project, repo=repo)
release.set_commits([{"id": commit.key, "repository": repo.name}])

min_ago = before_now(minutes=1).isoformat()
event = self.store_event(
data={"fingerprint": ["group1"], "timestamp": min_ago},
project_id=project.id,
default_event_type=EventType.DEFAULT,
)
assert event.group is not None

GroupOwner.objects.create(
group_id=event.group.id,
project=project,
organization_id=project.organization_id,
type=GroupOwnerType.SUSPECT_COMMIT.value,
user_id=self.user.id,
context={
"commitId": commit.id,
"suspectCommitStrategy": SuspectCommitStrategy.RELEASE_BASED,
},
)

url = reverse(
"sentry-api-0-event-file-committers",
kwargs={
"event_id": event.event_id,
"project_id_or_slug": event.project.slug,
"organization_id_or_slug": event.project.organization.slug,
},
)

response = self.client.get(url, format="json")
assert response.status_code == 200, response.content
assert len(response.data["committers"]) == 1

commits = response.data["committers"][0]["commits"]
assert len(commits) == 1
assert commits[0]["id"] == commit.key
assert commits[0]["suspectCommitType"] == "via commit in release"
27 changes: 19 additions & 8 deletions tests/sentry/tasks/test_groupowner.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,8 @@ def test_update_existing_entries(self) -> None:
)

date_added_before_update = go.date_added
assert go.context == {"suspectCommitStrategy": SuspectCommitStrategy.RELEASE_BASED}
assert "commitId" in go.context
assert go.context["suspectCommitStrategy"] == SuspectCommitStrategy.RELEASE_BASED

process_suspect_commits(
event_id=self.event.event_id,
Expand All @@ -297,7 +298,8 @@ def test_update_existing_entries(self) -> None:
)
go.refresh_from_db()
assert go.date_added > date_added_before_update
assert go.context == {"suspectCommitStrategy": SuspectCommitStrategy.RELEASE_BASED}
assert "commitId" in go.context
assert go.context["suspectCommitStrategy"] == SuspectCommitStrategy.RELEASE_BASED
assert GroupOwner.objects.filter(group=self.event.group).count() == 1
assert GroupOwner.objects.get(
group=self.event.group,
Expand Down Expand Up @@ -341,9 +343,14 @@ def test_no_release_or_commit(self) -> None:
def test_keep_highest_score(self, patched_committers: MagicMock) -> None:
self.user2 = self.create_user(email="user2@sentry.io")
self.user3 = self.create_user(email="user3@sentry.io")

mock_commit1 = MagicMock(id=1)
mock_commit2 = MagicMock(id=2)
mock_commit3 = MagicMock(id=3)

patched_committers.return_value = [
{
"commits": [(None, 3)],
"commits": [(mock_commit1, 3)],
"author": {
"username": self.user.email,
"lastLogin": None,
Expand All @@ -367,7 +374,7 @@ def test_keep_highest_score(self, patched_committers: MagicMock) -> None:
},
},
{
"commits": [(None, 1)],
"commits": [(mock_commit2, 1)],
"author": {
"username": self.user2.email,
"lastLogin": None,
Expand All @@ -391,7 +398,7 @@ def test_keep_highest_score(self, patched_committers: MagicMock) -> None:
},
},
{
"commits": [(None, 2)],
"commits": [(mock_commit3, 2)],
"author": {
"username": self.user3.email,
"lastLogin": None,
Expand Down Expand Up @@ -424,17 +431,21 @@ def test_keep_highest_score(self, patched_committers: MagicMock) -> None:
project_id=self.event.project_id,
)
# Doesn't use self.user2 due to low score.
assert GroupOwner.objects.get(user_id=self.user.id)
assert GroupOwner.objects.get(user_id=self.user3.id)
user1_owner = GroupOwner.objects.get(user_id=self.user.id)
user3_owner = GroupOwner.objects.get(user_id=self.user3.id)
assert not GroupOwner.objects.filter(user_id=self.user2.id).exists()

assert user1_owner.context["commitId"] == 1
assert user3_owner.context["commitId"] == 3

@patch("sentry.tasks.groupowner.get_event_file_committers")
def test_low_suspect_committer_score(self, patched_committers: MagicMock) -> None:
self.user = self.create_user()
mock_commit = MagicMock(id=1)
patched_committers.return_value = [
{
# score < MIN_COMMIT_SCORE
"commits": [(None, 1)],
"commits": [(mock_commit, 1)],
"author": {
"id": self.user.id,
},
Expand Down
Loading