-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
feat(ecosystem): Adds Project Management integration SLOs #80719
base: master
Are you sure you want to change the base?
Changes from all commits
267ccd9
65a74ea
424ea58
46a81b1
0bff1c8
09fdac7
324b21c
e000263
cb387a3
09cc6fd
d25c861
d124f60
7ec9649
52726ea
9a64b20
72005db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -13,6 +13,10 @@ | |||||||||||||||||||||||||||||||||||||||||||
from sentry.integrations.api.serializers.models.integration import IntegrationSerializer | ||||||||||||||||||||||||||||||||||||||||||||
from sentry.integrations.base import IntegrationFeatures, IntegrationInstallation | ||||||||||||||||||||||||||||||||||||||||||||
from sentry.integrations.models.external_issue import ExternalIssue | ||||||||||||||||||||||||||||||||||||||||||||
from sentry.integrations.project_management.metrics import ( | ||||||||||||||||||||||||||||||||||||||||||||
ProjectManagementActionType, | ||||||||||||||||||||||||||||||||||||||||||||
ProjectManagementEvent, | ||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||
from sentry.integrations.services.integration import RpcIntegration, integration_service | ||||||||||||||||||||||||||||||||||||||||||||
from sentry.models.activity import Activity | ||||||||||||||||||||||||||||||||||||||||||||
from sentry.models.group import Group | ||||||||||||||||||||||||||||||||||||||||||||
|
@@ -162,62 +166,72 @@ | |||||||||||||||||||||||||||||||||||||||||||
if not integration or not org_integration: | ||||||||||||||||||||||||||||||||||||||||||||
return Response(status=404) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
if not self._has_issue_feature_on_integration(integration): | ||||||||||||||||||||||||||||||||||||||||||||
return Response( | ||||||||||||||||||||||||||||||||||||||||||||
{"detail": "This feature is not supported for this integration."}, status=400 | ||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||
with ProjectManagementEvent( | ||||||||||||||||||||||||||||||||||||||||||||
action_type=ProjectManagementActionType.LINK_EXTERNAL_ISSUE, | ||||||||||||||||||||||||||||||||||||||||||||
integration=integration, | ||||||||||||||||||||||||||||||||||||||||||||
).capture() as lifecycle: | ||||||||||||||||||||||||||||||||||||||||||||
if not self._has_issue_feature_on_integration(integration): | ||||||||||||||||||||||||||||||||||||||||||||
return Response( | ||||||||||||||||||||||||||||||||||||||||||||
{"detail": "This feature is not supported for this integration."}, status=400 | ||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
installation = integration.get_installation(organization_id=organization_id) | ||||||||||||||||||||||||||||||||||||||||||||
try: | ||||||||||||||||||||||||||||||||||||||||||||
data = installation.get_issue(external_issue_id, data=request.data) | ||||||||||||||||||||||||||||||||||||||||||||
except IntegrationFormError as exc: | ||||||||||||||||||||||||||||||||||||||||||||
return Response(exc.field_errors, status=400) | ||||||||||||||||||||||||||||||||||||||||||||
except IntegrationError as e: | ||||||||||||||||||||||||||||||||||||||||||||
return Response({"non_field_errors": [str(e)]}, status=400) | ||||||||||||||||||||||||||||||||||||||||||||
installation = integration.get_installation(organization_id=organization_id) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
defaults = { | ||||||||||||||||||||||||||||||||||||||||||||
"title": data.get("title"), | ||||||||||||||||||||||||||||||||||||||||||||
"description": data.get("description"), | ||||||||||||||||||||||||||||||||||||||||||||
"metadata": data.get("metadata"), | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
try: | ||||||||||||||||||||||||||||||||||||||||||||
data = installation.get_issue(external_issue_id, data=request.data) | ||||||||||||||||||||||||||||||||||||||||||||
except IntegrationFormError as exc: | ||||||||||||||||||||||||||||||||||||||||||||
lifecycle.record_halt(exc) | ||||||||||||||||||||||||||||||||||||||||||||
return Response(exc.field_errors, status=400) | ||||||||||||||||||||||||||||||||||||||||||||
except IntegrationError as e: | ||||||||||||||||||||||||||||||||||||||||||||
lifecycle.record_failure(e) | ||||||||||||||||||||||||||||||||||||||||||||
return Response({"non_field_errors": [str(e)]}, status=400) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
external_issue_key = installation.make_external_key(data) | ||||||||||||||||||||||||||||||||||||||||||||
external_issue, created = ExternalIssue.objects.get_or_create( | ||||||||||||||||||||||||||||||||||||||||||||
organization_id=organization_id, | ||||||||||||||||||||||||||||||||||||||||||||
integration_id=integration.id, | ||||||||||||||||||||||||||||||||||||||||||||
key=external_issue_key, | ||||||||||||||||||||||||||||||||||||||||||||
defaults=defaults, | ||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||
defaults = { | ||||||||||||||||||||||||||||||||||||||||||||
"title": data.get("title"), | ||||||||||||||||||||||||||||||||||||||||||||
"description": data.get("description"), | ||||||||||||||||||||||||||||||||||||||||||||
"metadata": data.get("metadata"), | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
if created: | ||||||||||||||||||||||||||||||||||||||||||||
integration_issue_linked.send_robust( | ||||||||||||||||||||||||||||||||||||||||||||
integration=integration, | ||||||||||||||||||||||||||||||||||||||||||||
organization=group.project.organization, | ||||||||||||||||||||||||||||||||||||||||||||
user=request.user, | ||||||||||||||||||||||||||||||||||||||||||||
sender=self.__class__, | ||||||||||||||||||||||||||||||||||||||||||||
external_issue_key = installation.make_external_key(data) | ||||||||||||||||||||||||||||||||||||||||||||
external_issue, created = ExternalIssue.objects.get_or_create( | ||||||||||||||||||||||||||||||||||||||||||||
organization_id=organization_id, | ||||||||||||||||||||||||||||||||||||||||||||
integration_id=integration.id, | ||||||||||||||||||||||||||||||||||||||||||||
key=external_issue_key, | ||||||||||||||||||||||||||||||||||||||||||||
defaults=defaults, | ||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||||||||||||||||
external_issue.update(**defaults) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
installation.store_issue_last_defaults(group.project, request.user, request.data) | ||||||||||||||||||||||||||||||||||||||||||||
try: | ||||||||||||||||||||||||||||||||||||||||||||
installation.after_link_issue(external_issue, data=request.data) | ||||||||||||||||||||||||||||||||||||||||||||
except IntegrationFormError as exc: | ||||||||||||||||||||||||||||||||||||||||||||
return Response(exc.field_errors, status=400) | ||||||||||||||||||||||||||||||||||||||||||||
except IntegrationError as e: | ||||||||||||||||||||||||||||||||||||||||||||
return Response({"non_field_errors": [str(e)]}, status=400) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
try: | ||||||||||||||||||||||||||||||||||||||||||||
with transaction.atomic(router.db_for_write(GroupLink)): | ||||||||||||||||||||||||||||||||||||||||||||
GroupLink.objects.create( | ||||||||||||||||||||||||||||||||||||||||||||
group_id=group.id, | ||||||||||||||||||||||||||||||||||||||||||||
project_id=group.project_id, | ||||||||||||||||||||||||||||||||||||||||||||
linked_type=GroupLink.LinkedType.issue, | ||||||||||||||||||||||||||||||||||||||||||||
linked_id=external_issue.id, | ||||||||||||||||||||||||||||||||||||||||||||
relationship=GroupLink.Relationship.references, | ||||||||||||||||||||||||||||||||||||||||||||
if created: | ||||||||||||||||||||||||||||||||||||||||||||
integration_issue_linked.send_robust( | ||||||||||||||||||||||||||||||||||||||||||||
integration=integration, | ||||||||||||||||||||||||||||||||||||||||||||
organization=group.project.organization, | ||||||||||||||||||||||||||||||||||||||||||||
user=request.user, | ||||||||||||||||||||||||||||||||||||||||||||
sender=self.__class__, | ||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||
except IntegrityError: | ||||||||||||||||||||||||||||||||||||||||||||
return Response({"non_field_errors": ["That issue is already linked"]}, status=400) | ||||||||||||||||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||||||||||||||||
external_issue.update(**defaults) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
installation.store_issue_last_defaults(group.project, request.user, request.data) | ||||||||||||||||||||||||||||||||||||||||||||
try: | ||||||||||||||||||||||||||||||||||||||||||||
installation.after_link_issue(external_issue, data=request.data) | ||||||||||||||||||||||||||||||||||||||||||||
except IntegrationFormError as exc: | ||||||||||||||||||||||||||||||||||||||||||||
lifecycle.record_halt(exc) | ||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are these errors user config errors? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, typically an |
||||||||||||||||||||||||||||||||||||||||||||
return Response(exc.field_errors, status=400) | ||||||||||||||||||||||||||||||||||||||||||||
except IntegrationError as e: | ||||||||||||||||||||||||||||||||||||||||||||
lifecycle.record_failure(e) | ||||||||||||||||||||||||||||||||||||||||||||
return Response({"non_field_errors": [str(e)]}, status=400) | ||||||||||||||||||||||||||||||||||||||||||||
Check warning Code scanning / CodeQL Information exposure through an exception Medium Stack trace information Error loading related location Loading
Copilot Autofix AI 2 days ago To fix the problem, we need to ensure that detailed exception messages are not exposed to the end user. Instead, we should log the detailed error message on the server and return a generic error message to the user. This can be achieved by modifying the exception handling code to log the exception and return a generic error message.
Suggested changeset
1
src/sentry/api/endpoints/group_integration_details.py
Copilot is powered by AI and may make mistakes. Always verify output.
Refresh and try again.
|
||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
try: | ||||||||||||||||||||||||||||||||||||||||||||
with transaction.atomic(router.db_for_write(GroupLink)): | ||||||||||||||||||||||||||||||||||||||||||||
GroupLink.objects.create( | ||||||||||||||||||||||||||||||||||||||||||||
group_id=group.id, | ||||||||||||||||||||||||||||||||||||||||||||
project_id=group.project_id, | ||||||||||||||||||||||||||||||||||||||||||||
linked_type=GroupLink.LinkedType.issue, | ||||||||||||||||||||||||||||||||||||||||||||
linked_id=external_issue.id, | ||||||||||||||||||||||||||||||||||||||||||||
relationship=GroupLink.Relationship.references, | ||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||
except IntegrityError as exc: | ||||||||||||||||||||||||||||||||||||||||||||
lifecycle.record_halt(exc) | ||||||||||||||||||||||||||||||||||||||||||||
return Response({"non_field_errors": ["That issue is already linked"]}, status=400) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
self.create_issue_activity(request, group, installation, external_issue, new=False) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
from dataclasses import dataclass | ||
from enum import Enum | ||
|
||
from sentry.integrations.base import IntegrationDomain | ||
from sentry.integrations.models import Integration | ||
from sentry.integrations.services.integration import RpcIntegration | ||
from sentry.integrations.utils.metrics import IntegrationEventLifecycleMetric | ||
|
||
|
||
class ProjectManagementActionType(Enum): | ||
CREATE_EXTERNAL_ISSUE = "create_external_issue" | ||
OUTBOUND_ASSIGNMENT_SYNC = "outbound_assignment_sync" | ||
INBOUND_ASSIGNMENT_SYNC = "inbound_assignment_sync" | ||
COMMENT_SYNC = "comment_sync" | ||
OUTBOUND_STATUS_SYNC = "outbound_status_sync" | ||
INBOUND_STATUS_SYNC = "inbound_status_sync" | ||
LINK_EXTERNAL_ISSUE = "link_external_issue" | ||
|
||
def __str__(self): | ||
return self.value.lower() | ||
|
||
|
||
@dataclass | ||
class ProjectManagementEvent(IntegrationEventLifecycleMetric): | ||
action_type: ProjectManagementActionType | ||
integration: Integration | RpcIntegration | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Turns out we don't always have access to integration installations (specifically the inbound assignment code), so I made this use integration/rpc integration. |
||
|
||
def get_integration_name(self) -> str: | ||
return self.integration.provider | ||
|
||
def get_integration_domain(self) -> IntegrationDomain: | ||
return IntegrationDomain.PROJECT_MANAGEMENT | ||
|
||
def get_interaction_type(self) -> str: | ||
return str(self.action_type) |
Check warning
Code scanning / CodeQL
Information exposure through an exception Medium
Copilot Autofix AI 2 days ago
To fix the problem, we should avoid exposing the detailed exception message to the end user. Instead, we can log the detailed error message on the server for debugging purposes and return a generic error message to the user. This approach ensures that sensitive information is not exposed while still allowing developers to diagnose issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't new code, this is what allows errors to propagate up to the UI when ticket creation or linking fails on the integrator side (usually a config issue with the ticket contents)