src/sentry/middleware/integrations/integration_control_middleware.md
Hybrid Cloud requires running Sentry in two different instances which communicate with one another; Control and Cell Silos. The integration authentication data (Integration, and OrganizationIntegration models) will be stored in the Control Silo, but the associated models integrations may affect will be stored in the Cell Silo (e.g. Repository, Commit, ExternalIssue, Organization, etc.).
Incoming webhooks fired by integration providers notify us when changes occur in their system (e.g. someone assigns an issue in slack, or a PR resolving an issue is merged on GitHub). These are always received by the Control Silo, so we need parsers to intercept these requests to forward the data to the relevant silos.
The magic happens in the IntegrationControlMiddleware. Here, we do the following steps:
/extensions/* (which is the prefix for all our webhooks) it is further inspected. If not, we fall through this middleware./extensions/provider/webhook-path/. If no parser is registered, we fall through this middlewareBaseRequestParser), we defer to it for responding to the request, rather than falling through.The parsers vary per integration but they follow the same basic steps:
Integration object from the request.OrganizationIntegrationsOrganizationMappings.ControlOutbox model, (e.g. GitHub).The example of an integration parser may look something like this:
class ExampleRequestParser(BaseRequestParser):
provider = "example" # will match `/extensions/example/*` request paths
def get_integration_from_request(self) -> Integration | None:
integration_id = self.request.headers.get("X-Sentry-Integration-Id")
return Integration.objects.filter(id=integration_id).first()
def get_response(self):
# You can use the url router to identify the endpoint/view the request is headed to
if self.view_class in [ExampleConfigureView, ExampleSetupView]:
return self.get_response_from_control_silo()
# This method calls self.get_organizations_from_integration which calls self.get_integration_from_request.
cells = self.get_cells_from_organizations()
# If we're getting responses from multiple cells asynchronously...
if '/async/' in self.request.path:
return self.get_response_from_webhookpayload(cells=cells)
# If we're getting responses from multiple cells synchronously...
response_map = self.get_responses_from_cell_silos(cells=cells)
# Require all forwarded requests to succeed...
if not all([result.error is None for result in response_map.values()])
return HttpResponse(status_code=200)
You can register it and start forwarding requests when Sentry is running as a Control Silo by adding it to integeration_control.py:
- ACTIVE_PARSERS = [SlackRequestParser]
+ ACTIVE_PARSERS = [SlackRequestParser, ExampleRequestParser]