-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
feat(control): Add support for dropping unprocessed GitHub webhook events #109354
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
Changes from all commits
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 |
|---|---|---|
|
|
@@ -14,7 +14,11 @@ | |
| GitHubIntegrationsWebhookEndpoint, | ||
| get_github_external_id, | ||
| ) | ||
| from sentry.integrations.github.webhook_types import GITHUB_WEBHOOK_TYPE_HEADER, GithubWebhookType | ||
| from sentry.integrations.github.webhook_types import ( | ||
| GITHUB_WEBHOOK_TYPE_HEADER, | ||
| REGION_PROCESSED_GITHUB_EVENTS, | ||
| GithubWebhookType, | ||
| ) | ||
| from sentry.integrations.middleware.hybrid_cloud.parser import BaseRequestParser | ||
| from sentry.integrations.models.integration import Integration | ||
| from sentry.integrations.services.integration.model import RpcIntegration | ||
|
|
@@ -131,6 +135,26 @@ def get_response(self) -> HttpResponseBase: | |
| if codecov_regions: | ||
| self.try_forward_to_codecov(event=event) | ||
|
|
||
| github_event = self.request.META.get(GITHUB_WEBHOOK_TYPE_HEADER) | ||
| mailbox_name = f"{self.provider}:{self.get_mailbox_identifier(integration, event)}" | ||
| allowlist = options.get("github.webhook.drop-unprocessed-events.mailbox-allowlist") or () | ||
| mailbox_in_allowlist = any(mailbox_name == m for m in allowlist) | ||
|
|
||
| # Only drop when we have a known unprocessed event type. Missing or empty | ||
| # X-GitHub-Event is malformed; let the request be forwarded so the region | ||
| # returns 400 and GitHub is notified of the delivery failure. | ||
| if ( | ||
| github_event | ||
| and github_event not in REGION_PROCESSED_GITHUB_EVENTS | ||
| and options.get("github.webhook.drop-unprocessed-events.enabled") | ||
| and mailbox_in_allowlist | ||
| ): | ||
| metrics.incr( | ||
| "github.webhook.drop_unprocessed_event", | ||
| tags={"event_type": github_event or "unknown"}, | ||
| ) | ||
| return HttpResponse(status=202) | ||
|
|
||
|
Contributor
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. Redundant get_mailbox_identifier double-call inflates metricsLow Severity
|
||
| response = self.get_response_from_webhookpayload( | ||
| regions=regions, | ||
| identifier=self.get_mailbox_identifier(integration, event), | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,3 +1,5 @@ | ||||||||
| from unittest.mock import Mock, patch | ||||||||
|
|
||||||||
| import pytest | ||||||||
| import responses | ||||||||
| from django.db import router, transaction | ||||||||
|
|
@@ -411,6 +413,175 @@ def test_webhook_without_repository_falls_back_to_single_mailbox(self) -> None: | |||||||
| ) | ||||||||
|
|
||||||||
|
|
||||||||
| @control_silo_test | ||||||||
| class GithubRequestParserDropUnprocessedEventsTest(TestCase): | ||||||||
| """Tests for dropping GitHub webhook events that the region does not process.""" | ||||||||
|
|
||||||||
| factory = RequestFactory() | ||||||||
| path = reverse("sentry-integration-github-webhook") | ||||||||
|
|
||||||||
| def get_response(self, req: HttpRequest) -> HttpResponse: | ||||||||
| return HttpResponse(status=200, content="passthrough") | ||||||||
|
|
||||||||
| def get_integration(self) -> Integration: | ||||||||
| return self.create_integration( | ||||||||
| organization=self.organization, | ||||||||
| external_id="1", | ||||||||
| provider="github", | ||||||||
| ) | ||||||||
|
|
||||||||
| @override_settings(SILO_MODE=SiloMode.CONTROL) | ||||||||
| @override_regions(region_config) | ||||||||
| @responses.activate | ||||||||
| @patch("sentry.middleware.integrations.parsers.github.metrics") | ||||||||
| def test_drops_unprocessed_event_when_flag_and_allowlist(self, mock_metrics: Mock) -> None: | ||||||||
| """With flag on and mailbox in allowlist, status event is dropped and metric is incremented.""" | ||||||||
| integration = self.get_integration() | ||||||||
| mailbox_name = f"github:{integration.id}" | ||||||||
| with override_options( | ||||||||
| { | ||||||||
| "github.webhook.drop-unprocessed-events.enabled": True, | ||||||||
| "github.webhook.drop-unprocessed-events.mailbox-allowlist": [mailbox_name], | ||||||||
| } | ||||||||
| ): | ||||||||
| request = self.factory.post( | ||||||||
| self.path, | ||||||||
| data={"installation": {"id": "1"}, "repository": {"id": 123}}, | ||||||||
| content_type="application/json", | ||||||||
| headers={"X-GITHUB-EVENT": "status"}, | ||||||||
|
Member
Author
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. We don't handle |
||||||||
| ) | ||||||||
| parser = GithubRequestParser(request=request, response_handler=self.get_response) | ||||||||
| response = parser.get_response() | ||||||||
|
|
||||||||
| assert isinstance(response, HttpResponse) | ||||||||
| assert response.status_code == status.HTTP_202_ACCEPTED | ||||||||
| assert_no_webhook_payloads() | ||||||||
|
Member
Author
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. sentry/src/sentry/testutils/outbox.py Lines 59 to 61 in 2e27018
|
||||||||
| mock_metrics.incr.assert_any_call( | ||||||||
| "github.webhook.drop_unprocessed_event", | ||||||||
|
Member
Author
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. We dropped it and track it. |
||||||||
| tags={"event_type": "status"}, | ||||||||
| ) | ||||||||
|
|
||||||||
| @override_settings(SILO_MODE=SiloMode.CONTROL) | ||||||||
| @override_regions(region_config) | ||||||||
| @override_options( | ||||||||
| { | ||||||||
| "github.webhook.drop-unprocessed-events.enabled": False, | ||||||||
| "github.webhook.drop-unprocessed-events.mailbox-allowlist": ["github:99999"], | ||||||||
| } | ||||||||
| ) | ||||||||
| @responses.activate | ||||||||
| def test_does_not_drop_when_flag_off_creates_payloads(self) -> None: | ||||||||
| """With flag off, unprocessed event still creates WebhookPayloads.""" | ||||||||
| integration = self.get_integration() | ||||||||
| request = self.factory.post( | ||||||||
| self.path, | ||||||||
| data={"installation": {"id": "1"}, "repository": {"id": 123}}, | ||||||||
| content_type="application/json", | ||||||||
| headers={"X-GITHUB-EVENT": "status"}, | ||||||||
| ) | ||||||||
| parser = GithubRequestParser(request=request, response_handler=self.get_response) | ||||||||
| response = parser.get_response() | ||||||||
|
|
||||||||
| assert isinstance(response, HttpResponse) | ||||||||
| assert response.status_code == status.HTTP_202_ACCEPTED | ||||||||
| assert_webhook_payloads_for_mailbox( | ||||||||
|
Member
Author
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. It got recorded into the table even though it's a |
||||||||
| request=request, | ||||||||
| mailbox_name=f"github:{integration.id}", | ||||||||
| region_names=[region.name], | ||||||||
| ) | ||||||||
|
|
||||||||
| @override_settings(SILO_MODE=SiloMode.CONTROL) | ||||||||
| @override_regions(region_config) | ||||||||
| @override_options( | ||||||||
| { | ||||||||
| "github.webhook.drop-unprocessed-events.enabled": True, | ||||||||
| "github.webhook.drop-unprocessed-events.mailbox-allowlist": ["github:other"], | ||||||||
| } | ||||||||
| ) | ||||||||
| @responses.activate | ||||||||
| def test_does_not_drop_when_mailbox_not_in_allowlist_creates_payloads(self) -> None: | ||||||||
| """With mailbox not in allowlist, unprocessed event still creates WebhookPayloads.""" | ||||||||
| integration = self.get_integration() | ||||||||
| request = self.factory.post( | ||||||||
| self.path, | ||||||||
| data={"installation": {"id": "1"}, "repository": {"id": 123}}, | ||||||||
| content_type="application/json", | ||||||||
| headers={"X-GITHUB-EVENT": "status"}, | ||||||||
| ) | ||||||||
| parser = GithubRequestParser(request=request, response_handler=self.get_response) | ||||||||
| response = parser.get_response() | ||||||||
|
|
||||||||
| assert isinstance(response, HttpResponse) | ||||||||
| assert response.status_code == status.HTTP_202_ACCEPTED | ||||||||
| assert_webhook_payloads_for_mailbox( | ||||||||
|
Member
Author
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. Written to DB as expected. |
||||||||
| request=request, | ||||||||
| mailbox_name=f"github:{integration.id}", | ||||||||
| region_names=[region.name], | ||||||||
| ) | ||||||||
|
|
||||||||
| @override_settings(SILO_MODE=SiloMode.CONTROL) | ||||||||
| @override_regions(region_config) | ||||||||
| @responses.activate | ||||||||
| def test_supported_event_never_dropped_even_with_flag_and_allowlist(self) -> None: | ||||||||
| """Supported event (push) is never dropped even when flag on and mailbox in allowlist.""" | ||||||||
| integration = self.get_integration() | ||||||||
| with override_options( | ||||||||
| { | ||||||||
| "github.webhook.drop-unprocessed-events.enabled": True, | ||||||||
| "github.webhook.drop-unprocessed-events.mailbox-allowlist": [ | ||||||||
| f"github:{integration.id}" | ||||||||
| ], | ||||||||
| } | ||||||||
| ): | ||||||||
| request = self.factory.post( | ||||||||
| self.path, | ||||||||
| data={"installation": {"id": "1"}, "repository": {"id": 123}}, | ||||||||
| content_type="application/json", | ||||||||
| headers={"X-GITHUB-EVENT": GithubWebhookType.PUSH.value}, | ||||||||
| ) | ||||||||
| parser = GithubRequestParser(request=request, response_handler=self.get_response) | ||||||||
| response = parser.get_response() | ||||||||
|
|
||||||||
| assert isinstance(response, HttpResponse) | ||||||||
| assert response.status_code == status.HTTP_202_ACCEPTED | ||||||||
| assert_webhook_payloads_for_mailbox( | ||||||||
|
Member
Author
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. Written to DB as expected. |
||||||||
| request=request, | ||||||||
| mailbox_name=f"github:{integration.id}", | ||||||||
| region_names=[region.name], | ||||||||
| ) | ||||||||
|
|
||||||||
| @override_settings(SILO_MODE=SiloMode.CONTROL) | ||||||||
| @override_regions(region_config) | ||||||||
| @responses.activate | ||||||||
| def test_missing_x_github_event_not_dropped_forwards_to_region(self) -> None: | ||||||||
| """Missing X-GitHub-Event is not dropped; request is forwarded so region returns 400.""" | ||||||||
| integration = self.get_integration() | ||||||||
| with override_options( | ||||||||
| { | ||||||||
| "github.webhook.drop-unprocessed-events.enabled": True, | ||||||||
| "github.webhook.drop-unprocessed-events.mailbox-allowlist": [ | ||||||||
| f"github:{integration.id}" | ||||||||
| ], | ||||||||
| } | ||||||||
| ): | ||||||||
| request = self.factory.post( | ||||||||
| self.path, | ||||||||
| data={"installation": {"id": "1"}, "repository": {"id": 123}}, | ||||||||
| content_type="application/json", | ||||||||
| # No X-GitHub-Event header | ||||||||
| ) | ||||||||
| parser = GithubRequestParser(request=request, response_handler=self.get_response) | ||||||||
| response = parser.get_response() | ||||||||
|
|
||||||||
| assert isinstance(response, HttpResponse) | ||||||||
| assert response.status_code == status.HTTP_202_ACCEPTED | ||||||||
| assert_webhook_payloads_for_mailbox( | ||||||||
| request=request, | ||||||||
| mailbox_name=f"github:{integration.id}", | ||||||||
| region_names=[region.name], | ||||||||
| ) | ||||||||
|
|
||||||||
|
|
||||||||
| @control_silo_test | ||||||||
| class GithubRequestParserTypeRoutingTest(GithubRequestParserTest): | ||||||||
| """ | ||||||||
|
|
||||||||


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.
Is the plan to mainline this once we are confident it has a positive effect?
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.
That's correct.