Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
483d127
feat(issues): Add Issue Action Log instrumentation for issue mutations
yuvmen May 27, 2026
fe724a4
fix(typing): Use ApiClient() instead of ambiguous client module import
yuvmen May 27, 2026
33847a7
fix(tests): Update metrics_tools tests for ApiClient import change
yuvmen May 27, 2026
e3e6df1
fix(issues): Use django cache for MCP app ID, distinguish unarchive f…
yuvmen May 28, 2026
131c628
fix(issues): Revert UNARCHIVE action type, skip MCP lookup when no ap…
yuvmen May 28, 2026
09c707a
fix(issues): Log unknown source instead of dropping actions without c…
yuvmen May 28, 2026
1b2710b
ref(issues): Add ActionSource.UNKNOWN enum value for missing context
yuvmen May 28, 2026
ca12eb5
fix(typing): Annotate source variable to satisfy mypy assignment check
yuvmen May 28, 2026
5afd16b
feat(issues): Set action context for system and integration callers, …
yuvmen May 28, 2026
2f23cc3
ref(issues): Add all integration providers to ActionSource enum
yuvmen May 28, 2026
a114751
ref(issues): Address review feedback on action log instrumentation
yuvmen May 29, 2026
8dd06ad
fix(issues): Fix missing actor_id in ingest.py, revert test-env raise
yuvmen May 29, 2026
ab0f6b4
docs(issues): Add documentation to action_log module and public inter…
yuvmen Jun 1, 2026
d475d46
ref(issues): Move cache import to top level, add seer priority test
yuvmen Jun 1, 2026
fca57b6
ref(issues): Rename to Group Action Log, use patch.object with autosp…
yuvmen Jun 2, 2026
1fba4f0
ref(issues): Identify MCP source via User-Agent header
yuvmen Jun 2, 2026
90f205f
ref(issues): Resolve MCP client sub-type from client-family header
yuvmen Jun 2, 2026
e1aecec
fix(issues): Attribute commit/PR self-assign to the acting user
yuvmen Jun 2, 2026
b2c43ce
ref(issues): Defer autofix action-log instrumentation
yuvmen Jun 2, 2026
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
139 changes: 95 additions & 44 deletions src/sentry/api/helpers/group_index/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@
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,
action_context_scope,
get_action_context,
publish_action,
publish_action_from_context,
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 @@ -205,64 +213,74 @@ def update_groups(
if discard:
return handle_discard(request, groups, projects, acting_user)

status_details = result.pop("statusDetails", result)
status = result.get("status")
res_type = None
if "priority" in result:
if any(not group.issue_type.enable_user_status_and_priority_changes for group in groups):
return Response(
{"detail": "Cannot manually set priority of one or more issues."},
status=HTTPStatus.BAD_REQUEST,
)
source = resolve_action_source(request)
Comment thread
sentry-warden[bot] marked this conversation as resolved.
actor_id = acting_user.id if acting_user else None

with action_context_scope(source=source, actor_id=actor_id):
Comment thread
cursor[bot] marked this conversation as resolved.
status_details = result.pop("statusDetails", result)
status = result.get("status")
res_type = None
if "priority" in result:
if any(
not group.issue_type.enable_user_status_and_priority_changes for group in groups
):
return Response(
{"detail": "Cannot manually set priority of one or more issues."},
status=HTTPStatus.BAD_REQUEST,
)

handle_priority(
priority=result["priority"],
group_list=groups,
acting_user=acting_user,
project_lookup=project_lookup,
)
if status in ("resolved", "resolvedInNextRelease"):
if any(not group.issue_type.enable_user_status_and_priority_changes for group in groups):
return Response(
{"detail": "Cannot manually resolve one or more issues."},
status=HTTPStatus.BAD_REQUEST,
handle_priority(
priority=result["priority"],
group_list=groups,
acting_user=acting_user,
project_lookup=project_lookup,
)
if status in ("resolved", "resolvedInNextRelease"):
if any(
not group.issue_type.enable_user_status_and_priority_changes for group in groups
):
return Response(
{"detail": "Cannot manually resolve one or more issues."},
status=HTTPStatus.BAD_REQUEST,
)

try:
result, res_type = handle_resolve_in_release(
status,
status_details,
try:
result, res_type = handle_resolve_in_release(
status,
status_details,
groups,
projects,
project_lookup,
acting_user,
result,
)
except MultipleProjectsError:
return Response(
{"detail": "Cannot set resolved for multiple projects."}, status=400
)
elif status:
result = handle_other_status_updates(
result,
groups,
projects,
project_lookup,
status_details,
acting_user,
result,
)
except MultipleProjectsError:
return Response({"detail": "Cannot set resolved for multiple projects."}, status=400)
elif status:
result = handle_other_status_updates(

return prepare_response(
request,
result,
groups,
projects,
project_lookup,
status_details,
projects,
acting_user,
data,
res_type,
request.META.get("HTTP_REFERER", ""),
organization,
)

return prepare_response(
request,
result,
groups,
project_lookup,
projects,
acting_user,
data,
res_type,
request.META.get("HTTP_REFERER", ""),
organization,
)


def update_groups_with_search_fn(
request: Request,
Expand Down Expand Up @@ -628,6 +646,12 @@ def process_group_resolution(
data=dict(activity_data),
)
record_group_history_from_activity_type(group, activity_type, actor=acting_user)
publish_action_from_context(
action=ActionType.RESOLVE,
group_id=group.id,
organization_id=group.project.organization_id,
project_id=group.project_id,
)

# TODO(dcramer): we need a solution for activity rollups
# before sending notifications on bulk changes
Expand Down Expand Up @@ -794,6 +818,33 @@ def prepare_response(
acting_user,
urlparse(referer).path,
)
ctx = get_action_context()
if ctx 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=ctx.source,
group_id=primary_id,
organization_id=primary.project.organization_id,
project_id=primary.project_id,
actor_id=ctx.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=ctx.source,
group_id=child_id,
organization_id=child.project.organization_id,
project_id=child.project_id,
actor_id=ctx.actor_id,
metadata={"counterpart_group_id": primary_id},
)

inbox = result.get("inbox", None)
if inbox is not None:
Expand Down
21 changes: 12 additions & 9 deletions src/sentry/integrations/utils/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from sentry.integrations.services.assignment_source import AssignmentSource
from sentry.integrations.tasks.sync_assignee_outbound import sync_assignee_outbound
from sentry.integrations.types import EXTERNAL_PROVIDERS_REVERSE, ExternalProviderEnum
from sentry.issues.action_log import action_context_scope
from sentry.models.group import Group
from sentry.models.groupassignee import GroupAssignee
from sentry.models.organization import Organization
Expand Down Expand Up @@ -82,10 +83,11 @@ def _handle_deassign(
if not should_sync_assignee_inbound(group.organization, integration.provider):
continue

GroupAssignee.objects.deassign(
group,
assignment_source=AssignmentSource.from_integration(integration),
)
with action_context_scope(source=integration.provider, actor_id=None):
GroupAssignee.objects.deassign(
Comment thread
sentry[bot] marked this conversation as resolved.
group,
assignment_source=AssignmentSource.from_integration(integration),
)
groups_deassigned.append(group)
return groups_deassigned

Expand Down Expand Up @@ -116,11 +118,12 @@ def _handle_assign(
"user_id": user.id,
},
)
GroupAssignee.objects.assign(
group,
user,
assignment_source=AssignmentSource.from_integration(integration),
)
with action_context_scope(source=integration.provider, actor_id=None):
GroupAssignee.objects.assign(
group,
user,
assignment_source=AssignmentSource.from_integration(integration),
)
Comment on lines +121 to +126

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The action_context_scope in _handle_assign is incorrectly called with actor_id=None, causing user assignment actions from integrations to be misattributed to the system.
Severity: LOW

Suggested Fix

In _handle_assign, update the action_context_scope to use the available user's ID. Change with action_context_scope(source=integration.provider, actor_id=None): to with action_context_scope(source=integration.provider, actor_id=user.id):.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: src/sentry/integrations/utils/sync.py#L121-L126

Potential issue: In the `_handle_assign` function, the `action_context_scope` is invoked
with `actor_id=None` during integration syncs. A `user` object is available at this
point, but its ID is not used. This causes the action log to incorrectly attribute the
assignment to a system action with no actor, instead of the user being assigned. This
pattern was identified as a bug and fixed in other parts of the codebase
(`resolved_in_commit` and `resolved_in_pull_request`), but this instance was missed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

deferring to follow up

groups_assigned.append(group)
else:
logger.info(
Expand Down
Loading
Loading