Skip to content
Closed
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
169 changes: 169 additions & 0 deletions src/sentry/api/helpers/group_index/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from sentry.api.serializers.models.actor import ActorSerializer, ActorSerializerResponse
from sentry.hybridcloud.rpc import coerce_id_from
from sentry.integrations.tasks.kick_off_status_syncs import kick_off_status_syncs
from sentry.issues.action_log import ActionType, publish_action, resolve_action_source
from sentry.issues.grouptype import GroupCategory
from sentry.issues.ignored import handle_archived_until_escalating, handle_ignored
from sentry.issues.merge import MergedGroup, handle_merge
Expand Down Expand Up @@ -204,6 +205,9 @@ def update_groups(
if discard:
return handle_discard(request, groups, projects, acting_user)

source = resolve_action_source(request)
actor_id = acting_user.id if acting_user else None

status_details = result.pop("statusDetails", result)
status = result.get("status")
res_type = None
Expand All @@ -214,12 +218,26 @@ def update_groups(
status=HTTPStatus.BAD_REQUEST,
)

priority_value = PriorityLevel.from_str(result["priority"]) if result["priority"] else None
groups_with_priority_change = [
g for g in groups if priority_value is not None and g.priority != priority_value
]
handle_priority(
priority=result["priority"],
group_list=groups,
acting_user=acting_user,
project_lookup=project_lookup,
)
for group in groups_with_priority_change:
publish_action(
action=ActionType.SET_PRIORITY,
source=source,
group_id=group.id,
organization_id=group.project.organization_id,
project_id=group.project_id,
actor_id=actor_id,
metadata={"priority": result["priority"]},
)
Comment thread
cursor[bot] marked this conversation as resolved.
if status in ("resolved", "resolvedInNextRelease"):
if any(not group.issue_type.enable_user_status_and_priority_changes for group in groups):
return Response(
Expand All @@ -236,6 +254,8 @@ def update_groups(
project_lookup,
acting_user,
result,
source=source,
actor_id=actor_id,
)
except MultipleProjectsError:
return Response({"detail": "Cannot set resolved for multiple projects."}, status=400)
Expand All @@ -247,6 +267,8 @@ def update_groups(
project_lookup,
status_details,
acting_user,
source=source,
actor_id=actor_id,
)

return prepare_response(
Expand All @@ -260,6 +282,8 @@ def update_groups(
res_type,
request.META.get("HTTP_REFERER", ""),
organization,
source=source,
actor_id=actor_id,
)


Expand Down Expand Up @@ -371,6 +395,8 @@ def handle_resolve_in_release(
project_lookup: Mapping[int, Project],
acting_user: RpcUser | User | None,
result: MutableMapping[str, Any],
source: str | None = None,
actor_id: int | None = None,
) -> tuple[dict[str, Any], int | None]:
res_type = None
release = None
Expand Down Expand Up @@ -489,6 +515,17 @@ def handle_resolve_in_release(
sender=update_groups,
)

if source is not None:
publish_action(
action=ActionType.RESOLVE,
source=source,
group_id=group.id,
organization_id=projects[0].organization_id,
project_id=group.project_id,
actor_id=actor_id,
metadata={"resolution_type": res_type_str},
)

kick_off_status_syncs.apply_async(
kwargs={"project_id": group.project_id, "group_id": group.id}
)
Expand Down Expand Up @@ -719,6 +756,8 @@ def handle_other_status_updates(
project_lookup: Mapping[int, Project],
status_details: dict[str, Any],
acting_user: RpcUser | User | None,
source: str | None = None,
actor_id: int | None = None,
) -> dict[str, Any]:
group_ids = [group.id for group in group_list]
queryset = Group.objects.filter(id__in=group_ids)
Expand All @@ -729,6 +768,7 @@ def handle_other_status_updates(
new_substatus = infer_substatus(new_status, new_substatus, status_details, group_list)

with transaction.atomic(router.db_for_write(Group)):
changed_group_ids = set(queryset.exclude(status=new_status).values_list("id", flat=True))
status_updated = queryset.exclude(status=new_status).update(
status=new_status, substatus=new_substatus
)
Expand Down Expand Up @@ -764,6 +804,22 @@ def handle_other_status_updates(
status_details=result.get("statusDetails", {}),
sender=update_groups,
)

if source is not None:
action = (
ActionType.ARCHIVE if new_status == GroupStatus.IGNORED else ActionType.UNRESOLVE
)
for group in group_list:
if group.id in changed_group_ids:
publish_action(
action=action,
source=source,
group_id=group.id,
organization_id=group.project.organization_id,
project_id=group.project_id,
actor_id=actor_id,
)

return result


Expand All @@ -778,6 +834,8 @@ def prepare_response(
res_type: int | None,
referer: str,
organization: Organization | None = None,
source: str | None = None,
actor_id: int | None = None,
) -> Response:
# XXX (ahmed): hack to get the activities to work properly on issues page. Not sure of
# what performance impact this might have & this possibly should be moved else where
Expand All @@ -794,6 +852,10 @@ def prepare_response(
pass

if "assignedTo" in result:
prev_assignees = {
ga.group_id: (ga.user_id, ga.team_id)
for ga in GroupAssignee.objects.filter(group__in=group_list)
}
result["assignedTo"] = handle_assigned_to(
result["assignedTo"],
data.get("assignedBy"),
Expand All @@ -802,16 +864,79 @@ def prepare_response(
project_lookup,
acting_user,
)
if source is not None:
action = ActionType.ASSIGN if result["assignedTo"] else ActionType.UNASSIGN
for group in group_list:
had_assignee = group.id in prev_assignees
if action == ActionType.UNASSIGN and not had_assignee:
continue
if action == ActionType.ASSIGN:
cur = (
GroupAssignee.objects.filter(group=group)
.values_list("user_id", "team_id")
.first()
)
if cur and prev_assignees.get(group.id) == cur:
continue
publish_action(
action=action,
source=source,
group_id=group.id,
organization_id=group.project.organization_id,
project_id=group.project_id,
actor_id=actor_id,
)
Comment thread
cursor[bot] marked this conversation as resolved.

handle_has_seen(result.get("hasSeen"), group_list, project_lookup, projects, acting_user)

if "isBookmarked" in result:
already_bookmarked: set[int] = set()
if result["isBookmarked"] and source is not None and acting_user is not None:
already_bookmarked = set(
GroupBookmark.objects.filter(
group__in=group_list,
user_id=acting_user.id,
).values_list("group_id", flat=True)
)
handle_is_bookmarked(result["isBookmarked"], group_list, project_lookup, acting_user)
if source is not None and result["isBookmarked"]:
for group in group_list:
if group.id not in already_bookmarked:
publish_action(
action=ActionType.BOOKMARK,
source=source,
group_id=group.id,
organization_id=group.project.organization_id,
project_id=group.project_id,
actor_id=actor_id,
)

if result.get("isSubscribed") in (True, False):
prev_subscriptions: dict[int, bool] = {}
if source is not None and acting_user is not None:
prev_subscriptions = dict(
GroupSubscription.objects.filter(
group__in=group_list,
user_id=acting_user.id,
).values_list("group_id", "is_active")
)
result["subscriptionDetails"] = handle_is_subscribed(
result["isSubscribed"], group_list, project_lookup, acting_user
)
if source is not None:
action = ActionType.SUBSCRIBE if result["isSubscribed"] else ActionType.UNSUBSCRIBE
for group in group_list:
was_active = prev_subscriptions.get(group.id)
if was_active == result["isSubscribed"]:
continue
publish_action(
action=action,
source=source,
group_id=group.id,
organization_id=group.project.organization_id,
project_id=group.project_id,
actor_id=actor_id,
)
Comment thread
cursor[bot] marked this conversation as resolved.

if "isPublic" in result:
result["shareId"] = handle_is_public(
Expand All @@ -831,16 +956,60 @@ def prepare_response(
acting_user,
urlparse(referer).path,
)
if source is not None and isinstance(result["merge"], dict):
merged = result["merge"]
primary_id = int(merged["parent"])
child_ids = [int(c) for c in merged["children"]]
group_by_id = {g.id: g for g in group_list}
primary = group_by_id[primary_id]
publish_action(
action=ActionType.MERGE_FROM_OTHER,
source=source,
group_id=primary_id,
organization_id=primary.project.organization_id,
project_id=primary.project_id,
actor_id=actor_id,
metadata={"counterpart_group_ids": child_ids},
)
for child_id in child_ids:
child = group_by_id[child_id]
publish_action(
action=ActionType.MERGE_INTO_OTHER,
source=source,
group_id=child_id,
organization_id=child.project.organization_id,
project_id=child.project_id,
actor_id=actor_id,
metadata={"counterpart_group_id": primary_id},
)
Comment thread
cursor[bot] marked this conversation as resolved.

inbox = result.get("inbox", None)
if inbox is not None:
from sentry.models.groupinbox import GroupInbox

groups_in_inbox: set[int] = set()
if inbox is False:
groups_in_inbox = set(
GroupInbox.objects.filter(group__in=group_list).values_list("group_id", flat=True)
)
result["inbox"] = update_inbox(
inbox,
group_list,
project_lookup,
acting_user,
sender=update_groups,
)
if source is not None and inbox is False:
for group in group_list:
if group.id in groups_in_inbox:
publish_action(
action=ActionType.MARK_REVIEWED,
source=source,
group_id=group.id,
organization_id=group.project.organization_id,
project_id=group.project_id,
actor_id=actor_id,
)

# TODO(issues): This type is very fragile since it's fields are updated in quite a few places.
# Since this is a public API, we are using assuming a shape of MutateIssueResponse, but this
Expand Down
Loading
Loading