Skip to content
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
12 changes: 11 additions & 1 deletion src/sentry/issues/ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from sentry.utils import json, metrics, redis
from sentry.utils.strings import truncatechars
from sentry.utils.tag_normalization import normalized_sdk_tag_from_event
from sentry.viewer_context import ActorType, ViewerContext, viewer_context_scope
from sentry.workflow_engine.models import IncidentGroupOpenPeriod
from sentry.workflow_engine.processors.detector import (
associate_new_group_with_detector,
Expand Down Expand Up @@ -302,7 +303,16 @@ def save_issue_from_occurrence(
try:
# Since this calls hybrid cloud it has to be run outside the transaction
assignee = occurrence.assignee.resolve()
with action_context_scope(source=ActionSource.SYSTEM, actor=SYSTEM_ACTOR):
with (
viewer_context_scope(
ViewerContext(
organization_id=project.organization_id,
project_id=project.id,
actor_type=ActorType.SYSTEM,
)
),
action_context_scope(source=ActionSource.SYSTEM, actor=SYSTEM_ACTOR),
):
GroupAssignee.objects.assign(group, assignee, create_only=True)
except Exception:
logger.exception("Failed to process assignment for occurrence")
Expand Down
5 changes: 4 additions & 1 deletion src/sentry/models/groupassignee.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import contextvars
import logging
from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, TypedDict

Expand Down Expand Up @@ -197,8 +198,10 @@ def assign(
affected = True

if affected:
assignment_context = contextvars.copy_context()
transaction.on_commit(
lambda: issue_assigned.send_robust(
lambda: assignment_context.run(
issue_assigned.send_robust,
project=group.project,
group=group,
user=acting_user,
Expand Down
31 changes: 27 additions & 4 deletions src/sentry/receivers/releases.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from sentry.users.services.user import RpcUser
from sentry.users.services.user_option import get_option_from_list, user_option_service
from sentry.utils.cache import cache
from sentry.viewer_context import ActorType, ViewerContext, viewer_context_scope

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -219,8 +220,19 @@ def resolved_in_commit(instance: Commit, created, **kwargs):

if acting_user:
if self_assign_issue == "1" and not group.assignee_set.exists():
with action_context_scope(
source=ActionSource.SYSTEM, actor=GroupActionActor.user(acting_user.id)
with (
viewer_context_scope(
ViewerContext(
organization_id=group.project.organization_id,
project_id=group.project_id,
user_id=acting_user.id,
actor_type=ActorType.USER,
)
),
action_context_scope(
source=ActionSource.SYSTEM,
actor=GroupActionActor.user(acting_user.id),
),
):
GroupAssignee.objects.assign(
group=group, assigned_to=acting_user, acting_user=acting_user
Expand Down Expand Up @@ -315,8 +327,19 @@ def resolved_in_pull_request(instance: PullRequest, created, **kwargs):
linked_id=instance.id,
)
if acting_user:
with action_context_scope(
source=ActionSource.SYSTEM, actor=GroupActionActor.user(acting_user.id)
with (
viewer_context_scope(
ViewerContext(
organization_id=group.project.organization_id,
project_id=group.project_id,
user_id=acting_user.id,
actor_type=ActorType.USER,
)
),
action_context_scope(
source=ActionSource.SYSTEM,
actor=GroupActionActor.user(acting_user.id),
),
):
GroupAssignee.objects.assign(
group=group, assigned_to=acting_user, acting_user=acting_user
Expand Down
22 changes: 21 additions & 1 deletion tests/sentry/issues/test_ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from hashlib import md5
from typing import Any
from unittest import mock
from unittest.mock import patch

Expand Down Expand Up @@ -50,6 +51,7 @@
from sentry.utils import json
from sentry.utils.samples import load_data
from sentry.utils.snuba import raw_query
from sentry.viewer_context import ActorType, ViewerContext, get_viewer_context
from sentry.workflow_engine.models import DataCondition, DataConditionGroup, DataPacket
from sentry.workflow_engine.models.alertrule_detector import AlertRuleDetector
from sentry.workflow_engine.models.detector_group import DetectorGroup
Expand Down Expand Up @@ -161,11 +163,29 @@ def test_new_group_with_priority(self) -> None:
def test_new_group_with_user_assignee(self) -> None:
event = self.store_event(data={}, project_id=self.project.id)
occurrence = self.build_occurrence(event_id=event.event_id, assignee=f"user:{self.user.id}")
_, group_info = save_issue_occurrence(occurrence.to_dict(), event)
captured_contexts: list[ViewerContext | None] = []
assign = GroupAssignee.objects.assign

def capture_context(*args: Any, **kwargs: Any) -> Any:
captured_contexts.append(get_viewer_context())
return assign(*args, **kwargs)

with patch(
"sentry.issues.ingest.GroupAssignee.objects.assign", side_effect=capture_context
):
_, group_info = save_issue_occurrence(occurrence.to_dict(), event)

assert group_info is not None
assert group_info.group.priority == PriorityLevel.LOW
assignee = GroupAssignee.objects.get(group=group_info.group)
assert assignee.user_id == self.user.id
assert captured_contexts == [
ViewerContext(
organization_id=self.organization.id,
project_id=self.project.id,
actor_type=ActorType.SYSTEM,
)
]

def test_new_group_with_team_assignee(self) -> None:
event = self.store_event(data={}, project_id=self.project.id)
Expand Down
25 changes: 25 additions & 0 deletions tests/sentry/models/test_groupassignee.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,36 @@
from sentry.testutils.skips import requires_snuba
from sentry.types.activity import ActivityType
from sentry.users.services.user.service import user_service
from sentry.viewer_context import ActorType as ViewerActorType
from sentry.viewer_context import ViewerContext, get_viewer_context, viewer_context_scope

pytestmark = requires_snuba


class GroupAssigneeTestCase(TestCase):
def test_assign_preserves_viewer_context_until_on_commit(self) -> None:
expected_context = ViewerContext(
organization_id=self.organization.id,
project_id=self.project.id,
user_id=self.user.id,
actor_type=ViewerActorType.USER,
)
captured_contexts: list[ViewerContext | None] = []

with mock.patch("sentry.models.groupassignee.issue_assigned.send_robust") as send_robust:
send_robust.side_effect = lambda **kwargs: captured_contexts.append(
get_viewer_context()
)
with self.capture_on_commit_callbacks() as callbacks:
with viewer_context_scope(expected_context):
GroupAssignee.objects.assign(self.group, self.user)

assert get_viewer_context() is None
for callback in callbacks:
callback()

assert captured_contexts == [expected_context]

def test_constraints(self) -> None:
# Can't both be assigned
with pytest.raises(AssertionError):
Expand Down
37 changes: 35 additions & 2 deletions tests/sentry/receivers/test_releases.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from sentry.types.activity import ActivityType
from sentry.users.models.user_option import UserOption
from sentry.users.models.useremail import UserEmail
from sentry.viewer_context import ActorType, ViewerContext, get_viewer_context


class InvalidateReleaseCacheTest(TestCase):
Expand Down Expand Up @@ -326,7 +327,15 @@ def test_matching_author_with_assignment(self) -> None:
)
author.preload_users()

with self.assertLogs("sentry.issues.action_log", level="INFO") as logs:
captured_contexts: list[ViewerContext | None] = []
with (
self.assertLogs("sentry.issues.action_log", level="INFO") as logs,
patch("sentry.models.groupassignee.issue_assigned.send_robust") as send_robust,
self.capture_on_commit_callbacks(execute=True),
):
send_robust.side_effect = lambda **kwargs: captured_contexts.append(
get_viewer_context()
)
commit = Commit.objects.create(
key=sha1(uuid4().hex.encode("utf-8")).hexdigest(),
organization_id=group.organization.id,
Expand All @@ -338,6 +347,14 @@ def test_matching_author_with_assignment(self) -> None:
self.assertLinkedFromCommitDeferred(group, commit)

assert GroupAssignee.objects.filter(group=group, user_id=user.id).exists()
assert captured_contexts == [
ViewerContext(
organization_id=group.organization.id,
project_id=group.project_id,
user_id=user.id,
actor_type=ActorType.USER,
)
]

# The self-assign is attributed to the commit author, not logged as a system action.
assign_records = [r for r in logs.records if r.__dict__.get("action") == "assign"]
Expand Down Expand Up @@ -452,7 +469,15 @@ def test_matching_external_actor_sets_activity_user(self) -> None:
)
author = self._create_pull_request_author("newdev", group.organization.id)

pull_request = self._create_resolving_pull_request(group, repo, author)
captured_contexts: list[ViewerContext | None] = []
with (
patch("sentry.models.groupassignee.issue_assigned.send_robust") as send_robust,
self.capture_on_commit_callbacks(execute=True),
):
send_robust.side_effect = lambda **kwargs: captured_contexts.append(
get_viewer_context()
)
pull_request = self._create_resolving_pull_request(group, repo, author)

activity = Activity.objects.get(
group=group,
Expand All @@ -461,6 +486,14 @@ def test_matching_external_actor_sets_activity_user(self) -> None:
assert activity.user_id == user.id
assert activity.data == {"pull_request": pull_request.id}
assert GroupAssignee.objects.filter(group=group, user_id=user.id).exists()
assert captured_contexts == [
ViewerContext(
organization_id=group.organization.id,
project_id=group.project_id,
user_id=user.id,
actor_type=ActorType.USER,
)
]

@receivers_raise_on_send()
def test_author_from_different_organization_does_not_set_activity_user(self) -> None:
Expand Down
Loading