Skip to content

feat(issues): adds backend for "resolve in upcoming release" #70990

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 10 commits into from
Jun 18, 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
30 changes: 29 additions & 1 deletion src/sentry/api/helpers/group_index/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,6 @@ def update_groups(
)
if user_options:
self_assign_issue = user_options[0].value

if search_fn and not group_ids:
try:
cursor_result, _ = search_fn(
Expand Down Expand Up @@ -300,6 +299,34 @@ def update_groups(
res_type = GroupResolution.Type.in_next_release
res_type_str = "in_next_release"
res_status = GroupResolution.Status.pending
elif status_details.get("inUpcomingRelease"):
if len(projects) > 1:
return Response(
{"detail": "Cannot set resolved in upcoming release for multiple projects."},
status=400,
)
release = (
status_details.get("inUpcomingRelease")
or Release.objects.filter(
projects=projects[0], organization_id=projects[0].organization_id
)
.extra(select={"sort": "COALESCE(date_released, date_added)"})
.order_by("-sort")[0]
Comment on lines +312 to +314
Copy link
Member

@JoshFerge JoshFerge Jun 17, 2024

Choose a reason for hiding this comment

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

what does this mean semantically, and is this still the correct behavior for "resolve in upcoming release"? (the case where there is no release passed in.)

Copy link
Member Author

Choose a reason for hiding this comment

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

this query is used in a couple places to find the most recent release for a project, which we still need for this because in clear_expired_resolutions we compare the newly created release and check if there are any GroupResolution objects that have a release for a matching project that is older. if there is, then we know a new release for that project is created, so we can clear the pending GroupResolution and say the Group is resolved.

Copy link
Member

Choose a reason for hiding this comment

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

got it

)
activity_type = ActivityType.SET_RESOLVED_IN_RELEASE.value
activity_data = {"version": ""}

serialized_user = user_service.serialize_many(
filter=dict(user_ids=[user.id]), as_user=user
)
new_status_details = {
"inUpcomingRelease": True,
}
if serialized_user:
new_status_details["actor"] = serialized_user[0]
res_type = GroupResolution.Type.in_upcoming_release
res_type_str = "in_upcoming_release"
res_status = GroupResolution.Status.pending
elif status_details.get("inRelease"):
# TODO(jess): We could update validation to check if release
# applies to multiple projects, but I think we agreed to punt
Expand Down Expand Up @@ -606,6 +633,7 @@ def update_groups(
if res_type in (
GroupResolution.Type.in_next_release,
GroupResolution.Type.in_release,
GroupResolution.Type.in_upcoming_release,
):
result["activity"] = serialize(
Activity.objects.get_activities_for_group(
Expand Down
18 changes: 18 additions & 0 deletions src/sentry/api/helpers/group_index/validators/status_details.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from rest_framework import serializers

from sentry import features
from sentry.models.release import Release

from . import InCommitValidator


class StatusDetailsValidator(serializers.Serializer):
inNextRelease = serializers.BooleanField()
inUpcomingRelease = serializers.BooleanField()
inRelease = serializers.CharField()
inCommit = InCommitValidator(required=False)
ignoreDuration = serializers.IntegerField()
Expand Down Expand Up @@ -54,3 +56,19 @@ def validate_inNextRelease(self, value: bool) -> "Release":
raise serializers.ValidationError(
"No release data present in the system to form a basis for 'Next Release'"
)

def validate_inUpcomingRelease(self, value: bool) -> "Release":
project = self.context["project"]

if not features.has("organizations:resolve-in-upcoming-release", project.organization):
raise serializers.ValidationError(
"Your organization does not have access to this feature."
)
try:
return (
Release.objects.filter(projects=project, organization_id=project.organization_id)
.extra(select={"sort": "COALESCE(date_released, date_added)"})
.order_by("-sort")[0]
)
except IndexError:
raise serializers.ValidationError("No release data present in the system.")
1 change: 1 addition & 0 deletions src/sentry/models/groupresolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class GroupResolution(Model):
class Type:
in_release = 0
in_next_release = 1
in_upcoming_release = 2

class Status:
pending = 0
Expand Down
6 changes: 4 additions & 2 deletions src/sentry/tasks/clear_expired_resolutions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def clear_expired_resolutions(release_id):
the system that any pending resolutions older than the given release can now
be safely transitioned to resolved.

This is currently only used for ``in_next_release`` resolutions.
This is currently only used for ``in_next_release`` and ``in_upcoming_release`` resolutions.
"""
try:
release = Release.objects.get(id=release_id)
Expand All @@ -29,7 +29,9 @@ def clear_expired_resolutions(release_id):

resolution_list = list(
GroupResolution.objects.filter(
Q(type=GroupResolution.Type.in_next_release) | Q(type__isnull=True),
Q(type=GroupResolution.Type.in_next_release)
| Q(type__isnull=True)
| Q(type=GroupResolution.Type.in_upcoming_release),
release__projects__in=[p.id for p in release.projects.all()],
release__date_added__lt=release.date_added,
status=GroupResolution.Status.pending,
Expand Down
16 changes: 16 additions & 0 deletions tests/sentry/issues/endpoints/test_organization_group_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -4954,6 +4954,22 @@ def test_update_priority_no_change(self) -> None:
group=group2, type=ActivityType.SET_PRIORITY.value, user_id=self.user.id
).exists()

def test_resolved_in_upcoming_release_multiple_projects(self) -> None:
project_2 = self.create_project(slug="foo")
group1 = self.create_group(status=GroupStatus.UNRESOLVED)
group2 = self.create_group(status=GroupStatus.UNRESOLVED, project=project_2)

self.login_as(user=self.user)
response = self.get_response(
qs_params={
"id": [group1.id, group2.id],
"statd": "resolved",
"statusDetails": {"inUpcomingRelease": True},
}
)

assert response.status_code == 400


class GroupDeleteTest(APITestCase, SnubaTestCase):
endpoint = "sentry-api-0-organization-group-index"
Expand Down
79 changes: 78 additions & 1 deletion tests/snuba/api/endpoints/test_project_group_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from sentry.models.release import Release
from sentry.silo.base import SiloMode
from sentry.testutils.cases import APITestCase, SnubaTestCase
from sentry.testutils.helpers import parse_link_header
from sentry.testutils.helpers import parse_link_header, with_feature
from sentry.testutils.helpers.datetime import before_now, iso_format
from sentry.testutils.silo import assume_test_silo_mode
from sentry.types.activity import ActivityType
Expand Down Expand Up @@ -791,6 +791,83 @@ def test_set_resolved_in_next_release_legacy(self):
)
assert activity.data["version"] == ""

@with_feature("organizations:resolve-in-upcoming-release")
def test_set_resolved_in_upcoming_release(self):
release = Release.objects.create(organization_id=self.project.organization_id, version="a")
release.add_project(self.project)

group = self.create_group(status=GroupStatus.UNRESOLVED)

self.login_as(user=self.user)

url = f"{self.path}?id={group.id}"
response = self.client.put(
url,
data={"status": "resolved", "statusDetails": {"inUpcomingRelease": True}},
format="json",
)
assert response.status_code == 200
assert response.data["status"] == "resolved"
assert response.data["statusDetails"]["inUpcomingRelease"]
assert response.data["statusDetails"]["actor"]["id"] == str(self.user.id)
assert "activity" in response.data

group = Group.objects.get(id=group.id)
assert group.status == GroupStatus.RESOLVED

resolution = GroupResolution.objects.get(group=group)
assert resolution.release == release
assert resolution.type == GroupResolution.Type.in_upcoming_release
assert resolution.status == GroupResolution.Status.pending
assert resolution.actor_id == self.user.id

assert GroupSubscription.objects.filter(
user_id=self.user.id, group=group, is_active=True
).exists()

activity = Activity.objects.get(
group=group, type=ActivityType.SET_RESOLVED_IN_RELEASE.value
)
assert activity.data["version"] == ""

def test_upcoming_release_flag_validation(self):
release = Release.objects.create(organization_id=self.project.organization_id, version="a")
release.add_project(self.project)

group = self.create_group(status=GroupStatus.UNRESOLVED)

self.login_as(user=self.user)

url = f"{self.path}?id={group.id}"
response = self.client.put(
url,
data={"status": "resolved", "statusDetails": {"inUpcomingRelease": True}},
format="json",
)
assert response.status_code == 400
assert (
response.data["statusDetails"]["inUpcomingRelease"][0]
== "Your organization does not have access to this feature."
)

@with_feature("organizations:resolve-in-upcoming-release")
def test_upcoming_release_release_validation(self):
group = self.create_group(status=GroupStatus.UNRESOLVED)

self.login_as(user=self.user)

url = f"{self.path}?id={group.id}"
response = self.client.put(
url,
data={"status": "resolved", "statusDetails": {"inUpcomingRelease": True}},
format="json",
)
assert response.status_code == 400
assert (
response.data["statusDetails"]["inUpcomingRelease"][0]
== "No release data present in the system."
)

def test_set_resolved_in_explicit_commit_unreleased(self):
repo = self.create_repo(project=self.project, name=self.project.name)
commit = self.create_commit(project=self.project, repo=repo)
Expand Down
Loading