Skip to content
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

ref(mediators): Convert the other external request mediators #78662

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
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
8 changes: 4 additions & 4 deletions src/sentry/mediators/external_issues/issue_link_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

from sentry.coreapi import APIUnauthorized
from sentry.mediators.external_issues.creator import Creator
from sentry.mediators.external_requests.issue_link_requester import IssueLinkRequester
from sentry.mediators.mediator import Mediator
from sentry.mediators.param import Param
from sentry.models.group import Group
from sentry.sentry_apps.external_requests.issue_link_requester import IssueLinkRequester
from sentry.sentry_apps.models.platformexternalissue import PlatformExternalIssue
from sentry.sentry_apps.services.app import RpcSentryAppInstallation
from sentry.users.services.user import RpcUser
Expand All @@ -16,7 +16,7 @@ class IssueLinkCreator(Mediator):
install = Param(RpcSentryAppInstallation)
group = Param(Group)
action = Param(str)
fields = Param(object)
fields = Param(dict)
uri = Param(str)
user = Param(RpcUser)
using = router.db_for_write(PlatformExternalIssue)
Expand All @@ -32,14 +32,14 @@ def _verify_action(self):
raise APIUnauthorized(f"Invalid action '{self.action}'")

def _make_external_request(self):
self.response = IssueLinkRequester.run(
self.response = IssueLinkRequester(
install=self.install,
uri=self.uri,
group=self.group,
fields=self.fields,
user=self.user,
action=self.action,
)
).run()

def _create_external_issue(self):
self.external_issue = Creator.run(
Expand Down
2 changes: 0 additions & 2 deletions src/sentry/mediators/external_requests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
from .alert_rule_action_requester import AlertRuleActionRequester # NOQA
from .issue_link_requester import IssueLinkRequester # NOQA
from .select_requester import SelectRequester # NOQA
131 changes: 0 additions & 131 deletions src/sentry/mediators/external_requests/issue_link_requester.py

This file was deleted.

3 changes: 2 additions & 1 deletion src/sentry/mediators/external_requests/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from sentry.http import safe_urlopen
from sentry.sentry_apps.models.sentry_app import SentryApp, track_response_code
from sentry.sentry_apps.services.app.model import RpcSentryApp
from sentry.utils.sentry_apps import SentryAppWebhookRequestsBuffer
from sentry.utils.sentry_apps.webhooks import TIMEOUT_STATUS_CODE

Expand Down Expand Up @@ -49,7 +50,7 @@ def validate(instance, schema_type):

def send_and_save_sentry_app_request(
url: str,
sentry_app: SentryApp,
sentry_app: SentryApp | RpcSentryApp,
org_id: int,
event: str,
**kwargs: Any,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import region_silo_endpoint
from sentry.mediators.external_requests.select_requester import SelectRequester
from sentry.models.project import Project
from sentry.sentry_apps.api.bases.sentryapps import SentryAppInstallationBaseEndpoint
from sentry.sentry_apps.external_requests.select_requester import SelectRequester


@region_silo_endpoint
Expand Down Expand Up @@ -35,7 +35,7 @@ def get(self, request: Request, installation) -> Response:
kwargs.update({"project_slug": project.slug})

try:
choices = SelectRequester.run(**kwargs)
choices = SelectRequester(**kwargs).run()
except Exception:
return Response({"error": "Error communicating with Sentry App service"}, status=400)

Expand Down
6 changes: 3 additions & 3 deletions src/sentry/sentry_apps/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from django.utils.encoding import force_str
from django.utils.http import urlencode

from sentry.mediators.external_requests.select_requester import SelectRequester
from sentry.sentry_apps.external_requests.select_requester import SelectRequester
from sentry.sentry_apps.models.sentry_app_component import SentryAppComponent
from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation
from sentry.sentry_apps.services.app.model import RpcSentryAppComponent, RpcSentryAppInstallation
Expand Down Expand Up @@ -105,9 +105,9 @@ def _request(self, uri: str, dependent_data: str | None = None) -> Any:
install = self.install
if isinstance(install, SentryAppInstallation):
install = serialize_sentry_app_installation(install, install.sentry_app)
return SelectRequester.run(
return SelectRequester(
install=install,
project_slug=self.project_slug,
uri=uri,
dependent_data=dependent_data,
)
).run()
129 changes: 129 additions & 0 deletions src/sentry/sentry_apps/external_requests/issue_link_requester.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import logging
from dataclasses import dataclass
from typing import Any
from urllib.parse import urlparse
from uuid import uuid4

from django.db import router, transaction
from django.utils.functional import cached_property

from sentry.coreapi import APIError
from sentry.http import safe_urlread
from sentry.mediators.external_requests.util import send_and_save_sentry_app_request, validate
from sentry.models.group import Group
from sentry.sentry_apps.services.app import RpcSentryAppInstallation
from sentry.users.services.user import RpcUser
from sentry.utils import json

logger = logging.getLogger("sentry.sentry_apps.external_requests")
ACTION_TO_PAST_TENSE = {"create": "created", "link": "linked"}


@dataclass
class IssueLinkRequester:
"""
1. Makes a POST request to another service with data used for creating or
linking a Sentry issue to an issue in the other service.

The data sent to the other service is always in the following format:
{
'installationId': <install_uuid>,
'issueId': <sentry_group_id>,
'webUrl': <sentry_group_web_url>,
<fields>,
}

<fields> are any of the 'create' or 'link' form fields (determined by
the schema for that particular service)

2. Validates the response format from the other service and returns the
payload.

The data sent to the other service is always in the following format:
{
'identifier': <some_identifier>,
'webUrl': <external_issue_web_url>,
'project': <top_level_identifier>,
}

The project and identifier are use to generate the display text for the linked
issue in the UI (i.e. <project>#<identifier>)
"""

install: RpcSentryAppInstallation
uri: str
group: Group
fields: dict[str, Any]
user: RpcUser
action: str

def run(self) -> dict[str, Any]:
with transaction.atomic(router.db_for_write(Group)):
response: dict[str, str] = {}

try:
request = send_and_save_sentry_app_request(
self._build_url(),
self.sentry_app,
self.install.organization_id,
f"external_issue.{ACTION_TO_PAST_TENSE[self.action]}",
headers=self._build_headers(),
method="POST",
data=self.body,
)
body = safe_urlread(request)
response = json.loads(body)

except Exception as e:
logger.info(
"issue-link-requester.error",
extra={
"sentry_app": self.sentry_app.slug,
"install": self.install.uuid,
"project": self.group.project.slug,
"group": self.group.id,
"uri": self.uri,
"error_message": str(e),
},
)

if not self._validate_response(response):
raise APIError(
f"Invalid response format from sentry app {self.sentry_app} when linking issue"
)

return response

def _build_url(self) -> str:
urlparts = urlparse(self.sentry_app.webhook_url)
return f"{urlparts.scheme}://{urlparts.netloc}{self.uri}"

def _validate_response(self, resp: dict[str, str]) -> bool:
return validate(instance=resp, schema_type="issue_link")

def _build_headers(self) -> dict[str, str]:
request_uuid = uuid4().hex

return {
"Content-Type": "application/json",
"Request-ID": request_uuid,
"Sentry-App-Signature": self.sentry_app.build_signature(self.body),
}

@cached_property
def body(self):
body: dict[str, Any] = {
"fields": {},
"issueId": self.group.id,
"installationId": self.install.uuid,
"webUrl": self.group.get_absolute_url(),
"project": {"slug": self.group.project.slug, "id": self.group.project.id},
"actor": {"type": "user", "id": self.user.id, "name": self.user.name},
}
body["fields"].update(self.fields)

return json.dumps(body)

@cached_property
def sentry_app(self):
return self.install.sentry_app
Loading
Loading