src/sentry/shared_integrations/client/integration_proxy_client.md
Hybrid Cloud requires running Sentry in two different instances which communicate with one another; Control and Region 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 Region Silo (e.g. Repository, Commit, ExternalIssue, Organization, etc.).
Outbound integration requests can come from either silo type, region or control. For many integrations we will refresh our credentials if we receive a 403 response, or notice our existing token is expired prior to sending a request. Since integrations can be shared across regions, this introduces a problem. When refreshing credentials from two separate region silos, network latency can introduce race conditions and cause us to save incorrect tokens, breaking the auth exchange and locking up integrations. To resolve this, we use a proxy client to ensure all outbound requests exit the Control Silo and only add auth data just before leaving.
The proxying is managed by the IntegrationProxyClient. It inherits from the ApiClient to act as a drop in replacement, except that it requires an org_integration_id to __init__, and def authorize_request() must be implemented. Before any request made with the client, it checks which silo is creating the request:
self.authorize_request and proceeds as usual.{SENTRY_CONTROL_ADDRESS}/api/0/internal/integration-proxy # PROXY_BASE_PATH
self.authorize_request). The raw response is sent back to the originating silo to handle itself!Ensuring an integration proxies its requests can be done with three steps:
ApiClient base class with IntegrationProxyClient- class ExampleApiClient(ApiClient):
+ class ExampleApiClient(IntegrationProxyClient):
org_integration_id on __init__.def get_client(self):
return ExampleApiClient(org_integration_id=self.org_integration.id)
The helper method infer_org_integration may help if you only have integration_id context.
class ExampleApiClient(IntegrationProxyClient):
def __init__(
self,
integration_id: int,
org_integration_id: int | None = None
):
if org_integration_id is None:
org_integration_id = infer_org_integration(integration_id, logger)
super.__init__(org_integration_id)
authorize_request method. It should handle all token refreshes and authentication headers.@control_silo_function
def authorize_request(self, prepared_request: PreparedRequest) -> PreparedRequest:
integration = Integration.objects.filter(organizationintegration__id=self.org_integration_id).first()
if not integration:
return prepared_request
token_data = integration.metadata["auth_data"]
if token["expiration"] > deprecated_utcnow():
token_data = self._refresh_and_save_token_data()
prepared_request.headers["Authorization"] = f"Bearer {token_data["token"]}"
return prepared_request