Skip to content

Commit abe652c

Browse files
authored
Merge branch 'master' into ref/seer-remove-bulk-write-preferences
2 parents 4e61d78 + 5340eca commit abe652c

92 files changed

Lines changed: 1652 additions & 947 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎migrations_lockfile.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ releases: 0004_cleanup_failed_safe_deletes
3333

3434
replays: 0001_squashed_0007_organizationmember_replay_access
3535

36-
seer: 0042_breached_metric_automation
36+
seer: 0043_drop_seerprojectrepository_enabled
3737

3838
sentry: 1234_grouprelease_new_release_id_indexes
3939

‎pnpm-lock.yaml‎

Lines changed: 15 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/sentry/issues/comments.py‎

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import logging
2+
from datetime import timedelta
3+
from typing import Any
4+
5+
from django.utils import timezone
6+
7+
from sentry.integrations.tasks.create_comment import create_comment as create_external_comment
8+
from sentry.integrations.tasks.update_comment import update_comment as update_external_comment
9+
from sentry.issues.action_log import GroupActionActor, action_context_scope, publish_action
10+
from sentry.issues.action_log.types import CommentDeleteAction, CommentEditAction, SentryActorRef
11+
from sentry.models.activity import Activity
12+
from sentry.models.group import Group
13+
from sentry.models.grouplink import GroupLink
14+
from sentry.models.groupsubscription import GroupSubscription
15+
from sentry.notifications.types import GroupSubscriptionReason
16+
from sentry.signals import comment_created, comment_deleted, comment_updated
17+
from sentry.types.activity import ActivityType
18+
from sentry.users.models.user import User
19+
from sentry.users.services.user.model import RpcUser
20+
21+
logger = logging.getLogger(__name__)
22+
23+
24+
def _webhook_data(note: Activity) -> dict[str, Any]:
25+
return {
26+
"comment_id": note.id,
27+
"timestamp": note.datetime,
28+
"comment": note.data.get("text"),
29+
"project_slug": note.project.slug,
30+
}
31+
32+
33+
def _sync_comment(note: Activity, user_id: int, *, created: bool) -> None:
34+
assert note.group is not None
35+
task = create_external_comment if created else update_external_comment
36+
for external_issue_id in GroupLink.objects.filter(
37+
project_id=note.group.project_id,
38+
group_id=note.group_id,
39+
linked_type=GroupLink.LinkedType.issue,
40+
).values_list("linked_id", flat=True):
41+
task.apply_async(
42+
kwargs={
43+
"external_issue_id": external_issue_id,
44+
"group_note_id": note.id,
45+
"user_id": user_id,
46+
}
47+
)
48+
49+
50+
def create_comment(
51+
*,
52+
group: Group,
53+
user: User | RpcUser,
54+
data: dict[str, Any],
55+
source: str,
56+
from_email: bool = False,
57+
) -> Activity | None:
58+
"""
59+
Create a comment, preserving the existing inbound-email policy.
60+
61+
Email deduplicates within the project without a time limit and skips subscriptions,
62+
integration sync, and the comment_created signal. Activity notifications still run.
63+
"""
64+
duplicates = Activity.objects.filter(
65+
group=group, type=ActivityType.NOTE.value, user_id=user.id, data=data
66+
)
67+
if from_email:
68+
duplicates = duplicates.filter(project_id=group.project_id)
69+
else:
70+
duplicates = duplicates.filter(datetime__gte=timezone.now() - timedelta(hours=1))
71+
if duplicates.exists():
72+
return None
73+
74+
if not from_email:
75+
GroupSubscription.objects.subscribe(
76+
group=group, subscriber=user, reason=GroupSubscriptionReason.comment
77+
)
78+
79+
with action_context_scope(source=source, actor=GroupActionActor.user(user.id)):
80+
note = Activity.objects.create_group_activity(
81+
group=group, type=ActivityType.NOTE, user_id=user.id, data=data
82+
)
83+
if not from_email:
84+
_sync_comment(note, user.id, created=True)
85+
86+
if not from_email:
87+
comment_created.send_robust(
88+
project=group.project, user=user, group=group, data=_webhook_data(note), sender="post"
89+
)
90+
return note
91+
92+
93+
def update_comment(
94+
note: Activity,
95+
*,
96+
user: User | RpcUser,
97+
data: dict[str, Any],
98+
mentions: list[SentryActorRef],
99+
source: str,
100+
original_action_id: int | None,
101+
) -> Activity:
102+
assert note.group is not None
103+
note.data.update(data)
104+
note.save()
105+
if original_action_id is not None:
106+
publish_action(
107+
CommentEditAction(
108+
comment_id=original_action_id, text=data.get("text"), mentions=mentions
109+
),
110+
source=source,
111+
group_id=note.group_id,
112+
project=note.group.project,
113+
actor=GroupActionActor.user(user.id),
114+
)
115+
else:
116+
logger.info("group_notes.groupactionlogentry.not_found", extra={"group_id": note.group_id})
117+
118+
if note.data.get("external_id"):
119+
_sync_comment(note, user.id, created=False)
120+
comment_updated.send_robust(
121+
project=note.group.project,
122+
user=user,
123+
group=note.group,
124+
data=_webhook_data(note),
125+
sender="put",
126+
)
127+
return note
128+
129+
130+
def delete_comment(
131+
note: Activity, *, user: User | RpcUser, source: str, original_action_id: int | None
132+
) -> None:
133+
group = note.group
134+
assert group is not None
135+
notes_by_user = Activity.objects.filter(
136+
group=group, type=ActivityType.NOTE.value, user_id=user.id
137+
)
138+
last_comment = notes_by_user.count() == 1
139+
webhook_data = _webhook_data(note)
140+
note.delete()
141+
if original_action_id is not None:
142+
publish_action(
143+
CommentDeleteAction(comment_id=original_action_id),
144+
source=source,
145+
group_id=group.id,
146+
project=group.project,
147+
actor=GroupActionActor.user(user.id),
148+
)
149+
else:
150+
logger.info("group_notes.groupactionlogentry.not_found", extra={"group_id": group.id})
151+
comment_deleted.send_robust(
152+
project=group.project, user=user, group=group, data=webhook_data, sender="delete"
153+
)
154+
if last_comment:
155+
GroupSubscription.objects.filter(
156+
user_id=user.id,
157+
group=group,
158+
project=group.project,
159+
reason=GroupSubscriptionReason.comment,
160+
).delete()

‎src/sentry/issues/endpoints/bases/group.py‎

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from typing import Any
55

66
import sentry_sdk
7-
from django.db.models import QuerySet
87
from rest_framework.exceptions import PermissionDenied
98
from rest_framework.permissions import SAFE_METHODS
109
from rest_framework.request import Request
@@ -15,10 +14,7 @@
1514
from sentry.api.bases.project import ProjectPermission
1615
from sentry.api.exceptions import ResourceDoesNotExist
1716
from sentry.demo_mode.utils import is_demo_mode_enabled, is_demo_org, is_demo_user
18-
from sentry.integrations.tasks import create_comment, update_comment
19-
from sentry.models.activity import Activity
2017
from sentry.models.group import Group, GroupStatus, get_group_with_redirect
21-
from sentry.models.grouplink import GroupLink
2218
from sentry.models.organization import Organization
2319
from sentry.seer.seer_setup import has_seer_access_with_detail
2420
from sentry.utils.sdk import bind_organization_context
@@ -112,31 +108,6 @@ def convert_args(
112108

113109
return (args, kwargs)
114110

115-
def get_external_issue_ids(self, group: Group) -> QuerySet[Any]:
116-
return GroupLink.objects.filter(
117-
project_id=group.project_id, group_id=group.id, linked_type=GroupLink.LinkedType.issue
118-
).values_list("linked_id", flat=True)
119-
120-
def create_external_comment(self, request: Request, group: Group, group_note: Activity) -> None:
121-
for external_issue_id in self.get_external_issue_ids(group):
122-
create_comment.apply_async(
123-
kwargs={
124-
"external_issue_id": external_issue_id,
125-
"group_note_id": group_note.id,
126-
"user_id": request.user.id,
127-
}
128-
)
129-
130-
def update_external_comment(self, request: Request, group: Group, group_note: Activity) -> None:
131-
for external_issue_id in self.get_external_issue_ids(group):
132-
update_comment.apply_async(
133-
kwargs={
134-
"external_issue_id": external_issue_id,
135-
"group_note_id": group_note.id,
136-
"user_id": request.user.id,
137-
}
138-
)
139-
140111

141112
class GroupAiPermission(GroupPermission):
142113
scope_map = {

‎src/sentry/issues/endpoints/group_notes.py‎

Lines changed: 6 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import logging
2-
from datetime import timedelta
32

4-
from django.utils import timezone
53
from drf_spectacular.utils import extend_schema
64
from rest_framework import status
75
from rest_framework.exceptions import PermissionDenied
@@ -21,23 +19,20 @@
2119
from sentry.api.serializers.rest_framework.group_notes import NoteSerializer
2220
from sentry.apidocs.utils import inline_sentry_response_serializer
2321
from sentry.constants import CELL_API_DEPRECATION_DATE
24-
from sentry.issues.action_log import action_context_scope
2522
from sentry.issues.action_log.read_metrics import (
2623
ActivityReadFallbackReason,
2724
ActivityReadResult,
2825
activity_read_endpoint,
2926
record_activity_read,
3027
)
3128
from sentry.issues.action_log.request import resolve_action_source
32-
from sentry.issues.action_log.types import GroupActionActor, GroupActionType
29+
from sentry.issues.action_log.types import GroupActionType
30+
from sentry.issues.comments import create_comment
3331
from sentry.issues.derived.gate import should_serve_action_log_activity
3432
from sentry.issues.endpoints.bases.group import GroupEndpoint
3533
from sentry.issues.models.groupactionlogentry import GroupActionLogEntry
3634
from sentry.models.activity import Activity
3735
from sentry.models.group import Group
38-
from sentry.models.groupsubscription import GroupSubscription
39-
from sentry.notifications.types import GroupSubscriptionReason
40-
from sentry.signals import comment_created
4136
from sentry.types.activity import ActivityType
4237
from sentry.utils.action_log.activity_translator import activity_action_idempotency_key
4338

@@ -107,47 +102,15 @@ def post(self, request: Request, group: Group) -> Response:
107102
if "mentions" in data:
108103
data["mentions"] = [m.dict() for m in data["mentions"]]
109104

110-
if Activity.objects.filter(
111-
group=group,
112-
type=ActivityType.NOTE.value,
113-
user_id=request.user.id,
114-
data=data,
115-
datetime__gte=timezone.now() - timedelta(hours=1),
116-
).exists():
105+
activity = create_comment(
106+
group=group, user=request.user, data=data, source=resolve_action_source(request)
107+
)
108+
if activity is None:
117109
return Response(
118110
{"detail": "You have already posted that comment."},
119111
status=status.HTTP_400_BAD_REQUEST,
120112
)
121113

122-
GroupSubscription.objects.subscribe(
123-
group=group, subscriber=request.user, reason=GroupSubscriptionReason.comment
124-
)
125-
126-
with action_context_scope(
127-
source=resolve_action_source(request),
128-
actor=GroupActionActor.user(request.user.id),
129-
):
130-
activity = Activity.objects.create_group_activity(
131-
group=group, type=ActivityType.NOTE, user_id=request.user.id, data=data
132-
)
133-
134-
self.create_external_comment(request, group, activity)
135-
136-
webhook_data = {
137-
"comment_id": activity.id,
138-
"timestamp": activity.datetime,
139-
"comment": activity.data.get("text"),
140-
"project_slug": activity.project.slug,
141-
}
142-
143-
comment_created.send_robust(
144-
project=group.project,
145-
user=request.user,
146-
group=group,
147-
data=webhook_data,
148-
sender="post",
149-
)
150-
151114
endpoint = activity_read_endpoint(request)
152115
if should_serve_action_log_activity(group.project, request.user, endpoint=endpoint):
153116
entry = GroupActionLogEntry.objects.filter(

0 commit comments

Comments
 (0)