Skip to content

Commit bbe1145

Browse files
authored
fix(sentry-apps): Handle empty webhook_url in external requests (#109529)
1 parent 76c6f53 commit bbe1145

File tree

7 files changed

+183
-3
lines changed

7 files changed

+183
-3
lines changed

src/sentry/sentry_apps/components.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation
1414
from sentry.sentry_apps.services.app.model import RpcSentryAppComponent, RpcSentryAppInstallation
1515
from sentry.sentry_apps.services.app.serial import serialize_sentry_app_installation
16+
from sentry.sentry_apps.utils.errors import SentryAppIntegratorError
1617
from sentry.utils import json
1718

1819

@@ -35,6 +36,17 @@ def _prepare_stacktrace_link(self) -> None:
3536
schema = self.component.app_schema
3637
uri = schema.get("uri")
3738

39+
if not self.install.sentry_app.webhook_url:
40+
raise SentryAppIntegratorError(
41+
message="Sentry app webhook_url is not configured",
42+
webhook_context={
43+
"error_type": "MISSING_URL",
44+
"sentry_app_slug": self.install.sentry_app.slug,
45+
"uri": uri,
46+
},
47+
status_code=500,
48+
)
49+
3850
urlparts = list(urlparse(force_str(self.install.sentry_app.webhook_url)))
3951
urlparts[2] = str(uri)
4052

src/sentry/sentry_apps/external_requests/alert_rule_action_requester.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
)
2020
from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation
2121
from sentry.sentry_apps.services.app.model import RpcSentryAppInstallation
22-
from sentry.sentry_apps.utils.errors import SentryAppErrorType
22+
from sentry.sentry_apps.utils.errors import SentryAppErrorType, SentryAppIntegratorError
2323
from sentry.utils import json
2424

2525
DEFAULT_SUCCESS_MESSAGE = "Success!"
@@ -81,6 +81,15 @@ def run(self) -> SentryAppAlertRuleActionResult:
8181
webhook_context={"error_type": halt_reason, **extras},
8282
status_code=500,
8383
)
84+
except SentryAppIntegratorError as e:
85+
lifecycle.record_halt(halt_reason=e, extra={**extras})
86+
return SentryAppAlertRuleActionResult(
87+
success=False,
88+
message=e.message,
89+
error_type=SentryAppErrorType.INTEGRATOR,
90+
webhook_context={"error_type": e.webhook_context["error_type"], **extras},
91+
status_code=e.status_code,
92+
)
8493
except Exception as e:
8594
failure_reason = FAILURE_REASON_BASE.format(
8695
SentryAppExternalRequestFailureReason.UNEXPECTED_ERROR
@@ -102,6 +111,19 @@ def run(self) -> SentryAppAlertRuleActionResult:
102111
)
103112

104113
def _build_url(self) -> str:
114+
if not self.sentry_app.webhook_url:
115+
raise SentryAppIntegratorError(
116+
message="Sentry app webhook_url is not configured",
117+
webhook_context={
118+
"error_type": FAILURE_REASON_BASE.format(
119+
SentryAppExternalRequestFailureReason.MISSING_URL
120+
),
121+
"sentry_app_slug": self.sentry_app.slug,
122+
"uri": self.uri,
123+
},
124+
status_code=500,
125+
)
126+
105127
urlparts = list(urlparse(self.sentry_app.webhook_url))
106128
urlparts[2] = self.uri
107129
return urlunparse(urlparts)

src/sentry/sentry_apps/external_requests/issue_link_requester.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from sentry.sentry_apps.external_requests.utils import send_and_save_sentry_app_request, validate
1414
from sentry.sentry_apps.metrics import (
1515
SentryAppEventType,
16+
SentryAppExternalRequestFailureReason,
1617
SentryAppExternalRequestHaltReason,
1718
SentryAppInteractionEvent,
1819
SentryAppInteractionType,
@@ -125,6 +126,9 @@ def run(self) -> dict[str, Any]:
125126
webhook_context={"error_type": BAD_RESPONSE_HALT_REASON, **extras},
126127
status_code=500,
127128
)
129+
except SentryAppIntegratorError as e:
130+
lifecycle.record_halt(halt_reason=e, extra={**extras})
131+
raise
128132

129133
if not self._validate_response(response):
130134
extras["response"] = response
@@ -142,6 +146,19 @@ def run(self) -> dict[str, Any]:
142146
return response
143147

144148
def _build_url(self) -> str:
149+
if not self.sentry_app.webhook_url:
150+
raise SentryAppIntegratorError(
151+
message="Sentry app webhook_url is not configured",
152+
webhook_context={
153+
"error_type": FAILURE_REASON_BASE.format(
154+
SentryAppExternalRequestFailureReason.MISSING_URL
155+
),
156+
"sentry_app_slug": self.sentry_app.slug,
157+
"uri": self.uri,
158+
},
159+
status_code=500,
160+
)
161+
145162
urlparts = urlparse(self.sentry_app.webhook_url)
146163
return f"{urlparts.scheme}://{urlparts.netloc}{self.uri}"
147164

src/sentry/sentry_apps/external_requests/select_requester.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ def run(self) -> SelectRequesterResult:
9090
status_code=500,
9191
) from e
9292

93+
except SentryAppIntegratorError as e:
94+
lifecycle.record_halt(halt_reason=e, extra={**extras})
95+
raise
96+
9397
except Exception as e:
9498
failure_reason = FAILURE_REASON_BASE.format(
9599
SentryAppExternalRequestFailureReason.UNEXPECTED_ERROR
@@ -133,6 +137,19 @@ def run(self) -> SelectRequesterResult:
133137
raise
134138

135139
def _build_url(self) -> str:
140+
if not self.sentry_app.webhook_url:
141+
raise SentryAppIntegratorError(
142+
message="Sentry app webhook_url is not configured",
143+
webhook_context={
144+
"error_type": FAILURE_REASON_BASE.format(
145+
SentryAppExternalRequestFailureReason.MISSING_URL
146+
),
147+
"sentry_app_slug": self.sentry_app.slug,
148+
"uri": self.uri,
149+
},
150+
status_code=500,
151+
)
152+
136153
urlparts: list[str] = [url_part for url_part in urlparse(self.sentry_app.webhook_url)]
137154
urlparts[2] = self.uri
138155

tests/sentry/sentry_apps/external_requests/test_alert_rule_action_requester.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,3 +348,38 @@ def test_makes_failed_request_with_sentry_error(
348348
assert_many_failure_metrics(
349349
mock_record=mock_record, messages_or_errors=[Exception(), Exception()]
350350
)
351+
352+
@responses.activate
353+
@patch("sentry.sentry_apps.external_requests.utils.safe_urlopen")
354+
@patch("sentry.integrations.utils.metrics.EventLifecycle.record_event")
355+
def test_makes_failed_request_with_missing_url(
356+
self, mock_record: MagicMock, mock_urlopen: MagicMock
357+
) -> None:
358+
mock_urlopen.side_effect = Exception()
359+
self.sentry_app.webhook_url = ""
360+
self.sentry_app.save()
361+
362+
result = SentryAppAlertRuleActionRequester(
363+
install=self.install,
364+
uri="/sentry/alert-rule",
365+
fields=self.fields,
366+
).run()
367+
368+
assert not result["success"]
369+
assert result["message"] == "Sentry app webhook_url is not configured"
370+
assert result["error_type"] == SentryAppErrorType.INTEGRATOR
371+
assert result["webhook_context"] == {
372+
"error_type": FAILURE_REASON_BASE.format(
373+
SentryAppExternalRequestFailureReason.MISSING_URL
374+
),
375+
"uri": "/sentry/alert-rule",
376+
"installation_uuid": self.install.uuid,
377+
"sentry_app_slug": self.sentry_app.slug,
378+
}
379+
380+
assert_count_of_metric(
381+
mock_record=mock_record, outcome=EventLifecycleOutcome.STARTED, outcome_count=1
382+
)
383+
assert_count_of_metric(
384+
mock_record=mock_record, outcome=EventLifecycleOutcome.HALTED, outcome_count=1
385+
)

tests/sentry/sentry_apps/external_requests/test_issue_link_requester.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
IssueLinkRequester,
1111
IssueRequestActionType,
1212
)
13-
from sentry.sentry_apps.metrics import SentryAppEventType, SentryAppExternalRequestHaltReason
13+
from sentry.sentry_apps.metrics import (
14+
SentryAppEventType,
15+
SentryAppExternalRequestFailureReason,
16+
SentryAppExternalRequestHaltReason,
17+
)
18+
from sentry.sentry_apps.models.sentry_app import SentryApp
1419
from sentry.sentry_apps.services.app import app_service
1520
from sentry.sentry_apps.utils.errors import SentryAppIntegratorError
1621
from sentry.testutils.asserts import (
@@ -20,6 +25,7 @@
2025
assert_success_metric,
2126
)
2227
from sentry.testutils.cases import TestCase
28+
from sentry.testutils.silo import assume_test_silo_mode_of
2329
from sentry.users.services.user.serial import serialize_rpc_user
2430
from sentry.utils import json
2531
from sentry.utils.sentry_apps import SentryAppWebhookRequestsBuffer
@@ -267,3 +273,38 @@ def test_invalid_json_response(self, mock_record: MagicMock) -> None:
267273
assert_count_of_metric(
268274
mock_record=mock_record, outcome=EventLifecycleOutcome.HALTED, outcome_count=1
269275
)
276+
277+
@responses.activate
278+
@patch("sentry.integrations.utils.metrics.EventLifecycle.record_event")
279+
def test_no_webhook_url_configured_response(self, mock_record: MagicMock) -> None:
280+
with assume_test_silo_mode_of(SentryApp):
281+
self.sentry_app.webhook_url = ""
282+
self.sentry_app.save()
283+
284+
# Refresh install to ensure webhook_url is propagated correctly
285+
self.install = app_service.get_many(filter=dict(installation_ids=[self.orm_install.id]))[0]
286+
with pytest.raises(SentryAppIntegratorError) as exception_info:
287+
IssueLinkRequester(
288+
install=self.install,
289+
group=self.group,
290+
uri="/link-issue",
291+
fields={},
292+
user=self.rpc_user,
293+
action=IssueRequestActionType("create"),
294+
).run()
295+
assert exception_info.value.message == "Sentry app webhook_url is not configured"
296+
assert exception_info.value.webhook_context == {
297+
"error_type": FAILURE_REASON_BASE.format(
298+
SentryAppExternalRequestFailureReason.MISSING_URL
299+
),
300+
"uri": "/link-issue",
301+
"sentry_app_slug": self.sentry_app.slug,
302+
}
303+
304+
# SLO assertions
305+
assert_count_of_metric(
306+
mock_record=mock_record, outcome=EventLifecycleOutcome.STARTED, outcome_count=1
307+
)
308+
assert_count_of_metric(
309+
mock_record=mock_record, outcome=EventLifecycleOutcome.HALTED, outcome_count=1
310+
)

tests/sentry/sentry_apps/external_requests/test_select_requester.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
from requests import HTTPError
66

77
from sentry.integrations.types import EventLifecycleOutcome
8-
from sentry.sentry_apps.external_requests.select_requester import SelectRequester
8+
from sentry.sentry_apps.external_requests.select_requester import (
9+
FAILURE_REASON_BASE,
10+
SelectRequester,
11+
)
912
from sentry.sentry_apps.metrics import (
1013
SentryAppEventType,
1114
SentryAppExternalRequestFailureReason,
1215
SentryAppExternalRequestHaltReason,
1316
)
17+
from sentry.sentry_apps.models.sentry_app import SentryApp
1418
from sentry.sentry_apps.services.app import app_service
1519
from sentry.sentry_apps.utils.errors import SentryAppIntegratorError, SentryAppSentryError
1620
from sentry.testutils.asserts import (
@@ -21,6 +25,7 @@
2125
assert_success_metric,
2226
)
2327
from sentry.testutils.cases import TestCase
28+
from sentry.testutils.silo import assume_test_silo_mode_of
2429
from sentry.utils.sentry_apps import SentryAppWebhookRequestsBuffer
2530

2631

@@ -339,3 +344,34 @@ def test_unexpected_exception(
339344
assert_count_of_metric(
340345
mock_record=mock_record, outcome=EventLifecycleOutcome.FAILURE, outcome_count=1
341346
)
347+
348+
@responses.activate
349+
@patch("sentry.integrations.utils.metrics.EventLifecycle.record_event")
350+
def test_sentry_app_integrator_error(self, mock_record: MagicMock) -> None:
351+
with assume_test_silo_mode_of(SentryApp):
352+
self.sentry_app.webhook_url = ""
353+
self.sentry_app.save()
354+
self.install = app_service.get_many(filter=dict(installation_ids=[self.orm_install.id]))[0]
355+
356+
uri = "asdhbaljkdnaklskand"
357+
with pytest.raises(SentryAppIntegratorError) as exception_info:
358+
SelectRequester(
359+
install=self.install,
360+
project_slug=self.project.slug,
361+
uri=uri,
362+
).run()
363+
assert exception_info.value.message == "Sentry app webhook_url is not configured"
364+
assert exception_info.value.webhook_context == {
365+
"error_type": FAILURE_REASON_BASE.format(
366+
SentryAppExternalRequestFailureReason.MISSING_URL
367+
),
368+
"sentry_app_slug": self.sentry_app.slug,
369+
"uri": uri,
370+
}
371+
372+
assert_count_of_metric(
373+
mock_record=mock_record, outcome=EventLifecycleOutcome.STARTED, outcome_count=1
374+
)
375+
assert_count_of_metric(
376+
mock_record=mock_record, outcome=EventLifecycleOutcome.HALTED, outcome_count=1
377+
)

0 commit comments

Comments
 (0)