Skip to content

Commit c4988c9

Browse files
scttcperevanh
authored andcommitted
feat(integrations): Ignore duplicate group resolution (#82001)
1 parent fc65bd4 commit c4988c9

File tree

2 files changed

+105
-4
lines changed

2 files changed

+105
-4
lines changed

src/sentry/integrations/tasks/sync_status_inbound.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
from collections.abc import Iterable, Mapping
3+
from datetime import timedelta
34
from typing import Any
45

56
from django.db.models import Q
@@ -168,6 +169,20 @@ def get_resolutions_and_activity_data_for_groups(
168169
return resolutions_by_group_id, activity_type, activity_data
169170

170171

172+
def group_was_recently_resolved(group: Group) -> bool:
173+
"""
174+
Check if the group was resolved in the last 3 minutes
175+
"""
176+
if group.status != GroupStatus.RESOLVED:
177+
return False
178+
179+
try:
180+
group_resolution = GroupResolution.objects.get(group=group)
181+
return group_resolution.datetime > django_timezone.now() - timedelta(minutes=3)
182+
except GroupResolution.DoesNotExist:
183+
return False
184+
185+
171186
@instrumented_task(
172187
name="sentry.integrations.tasks.sync_status_inbound",
173188
queue="integrations",
@@ -189,8 +204,8 @@ def sync_status_inbound(
189204
raise Integration.DoesNotExist
190205

191206
organizations = Organization.objects.filter(id=organization_id)
192-
affected_groups = Group.objects.get_groups_by_external_issue(
193-
integration, organizations, issue_key
207+
affected_groups = list(
208+
Group.objects.get_groups_by_external_issue(integration, organizations, issue_key)
194209
)
195210
if not affected_groups:
196211
return
@@ -213,6 +228,17 @@ def sync_status_inbound(
213228
"integration_id": integration_id,
214229
}
215230
if action == ResolveSyncAction.RESOLVE:
231+
# Check if the group was recently resolved and we should skip the request
232+
# Avoid resolving the group in-app and then re-resolving via the integration webhook
233+
# which would override the in-app resolution
234+
resolvable_groups = []
235+
for group in affected_groups:
236+
if not group_was_recently_resolved(group):
237+
resolvable_groups.append(group)
238+
239+
if not resolvable_groups:
240+
return
241+
216242
(
217243
resolutions_by_group_id,
218244
activity_type,
@@ -221,14 +247,14 @@ def sync_status_inbound(
221247
affected_groups, config.get("resolution_strategy"), activity_data, organization_id
222248
)
223249
Group.objects.update_group_status(
224-
groups=affected_groups,
250+
groups=resolvable_groups,
225251
status=GroupStatus.RESOLVED,
226252
substatus=None,
227253
activity_type=activity_type,
228254
activity_data=activity_data,
229255
)
230256
# after we update the group, pdate the resolutions
231-
for group in affected_groups:
257+
for group in resolvable_groups:
232258
resolution_params = resolutions_by_group_id.get(group.id)
233259
if resolution_params:
234260
resolution, created = GroupResolution.objects.get_or_create(

tests/sentry/integrations/test_issues.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,81 @@ def test_sync_status_resolve_in_next_release_with_releases(self):
187187
"version": release2.version,
188188
}
189189

190+
def test_sync_status_does_not_override_existing_recent_group_resolution(self):
191+
"""
192+
Test that the sync_status_inbound does not override the existing group resolution
193+
if the group was recently resolved
194+
"""
195+
release = Release.objects.create(organization_id=self.project.organization_id, version="a")
196+
release2 = Release.objects.create(organization_id=self.project.organization_id, version="b")
197+
release.add_project(self.project)
198+
release2.add_project(self.project)
199+
group = self.create_group(status=GroupStatus.UNRESOLVED)
200+
# add releases in the reverse order
201+
self.create_group_release(group=group, release=release2)
202+
self.create_group_release(group=group, release=release)
203+
204+
assert group.status == GroupStatus.UNRESOLVED
205+
206+
# Resolve the group in old_release
207+
group.update(status=GroupStatus.RESOLVED, substatus=None)
208+
resolution = GroupResolution.objects.create(release=release, group=group)
209+
assert resolution.current_release_version is None
210+
assert resolution.release == release
211+
activity = Activity.objects.create(
212+
group=group,
213+
project=group.project,
214+
type=ActivityType.SET_RESOLVED_IN_RELEASE.value,
215+
ident=resolution.id,
216+
data={"version": release.version},
217+
)
218+
219+
with assume_test_silo_mode(SiloMode.CONTROL):
220+
integration = self.create_provider_integration(provider="example", external_id="123456")
221+
integration.add_organization(group.organization, self.user)
222+
223+
for oi in OrganizationIntegration.objects.filter(
224+
integration_id=integration.id, organization_id=group.organization.id
225+
):
226+
oi.update(
227+
config={
228+
"sync_comments": True,
229+
"sync_status_outbound": True,
230+
"sync_status_inbound": True,
231+
"sync_assignee_outbound": True,
232+
"sync_assignee_inbound": True,
233+
"resolution_strategy": "resolve_next_release",
234+
}
235+
)
236+
237+
external_issue = ExternalIssue.objects.create(
238+
organization_id=group.organization.id, integration_id=integration.id, key="APP-123"
239+
)
240+
241+
GroupLink.objects.create(
242+
group_id=group.id,
243+
project_id=group.project_id,
244+
linked_type=GroupLink.LinkedType.issue,
245+
linked_id=external_issue.id,
246+
relationship=GroupLink.Relationship.references,
247+
)
248+
249+
installation = integration.get_installation(group.organization.id)
250+
assert isinstance(installation, ExampleIntegration)
251+
252+
with self.feature("organizations:integrations-issue-sync"), self.tasks():
253+
installation.sync_status_inbound(
254+
external_issue.key,
255+
{"project_id": "APP", "status": {"id": "12345", "category": "done"}},
256+
)
257+
258+
assert Group.objects.get(id=group.id).status == GroupStatus.RESOLVED
259+
resolution.refresh_from_db()
260+
assert resolution.release == release
261+
assert resolution.current_release_version is None
262+
activity.refresh_from_db()
263+
assert activity.data["version"] == release.version
264+
190265
def test_sync_status_resolve_in_next_release_with_semver(self):
191266
release = Release.objects.create(
192267
organization_id=self.project.organization_id, version="app@1.2.4"

0 commit comments

Comments
 (0)