Project: Sentry (Observability and Analysis / Observability)
File: 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 . It inherits from the `ApiClient` to act as a drop in replacement, except that it requires an `orgintegrationid` to `init`, and `def authorize_request()` must be implemented. Before any request made with the client, it checks which silo is creating the request: If its in Monolith/Control mode, the client adds authentication data via `self.authorize_request` and proceeds as usual. If its in Region mode, the client does NOT add authentication data for the integration. Instead, it adds some headers () and sends the request to the control silo at a specific endpoint: ``` {SENTRYCONTROLADDRESS}/api/0/internal/integration-proxy # PROXYBASEPATH ``` The parses the headers to verify the request is coming from a valid Region silo, and then replays the request with the proper authentication data (from `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: Replace the `ApiClient` base class with `IntegrationProxyClient` ```diff class ExampleApiClient(ApiClient): class ExampleApiClient(IntegrationProxyClient): ``` Ensure all instances of the client pass in an `orgintegrationid` on `init`. ```python def get_client(self): return ExampleApiClient(orgintegrationid=self.org_integration.id) ``` The helper method may help if you only have `integration_id` context. ```python class ExampleApiClient(IntegrationProxyClient): def init( self, integration_id: int, orgintegrationid: int | None = None ): if orgintegrationid is None: orgintegrationid = inferorgintegration(integration_id, logger) super.init(orgintegrationid) ``` Implement the control-silo `authorize_request` method. It should handle all token refreshes and authentication headers. ```python @controlsilofunction def authorizerequest(self, preparedrequest: PreparedRequest) -> PreparedRequest: integration = Integration.objects.filter(organizationintegrationid=self.orgintegrationid).first() if not integration: return prepared_request tokendata = integration.metadata["authdata"] if token["expiration"] > datetime.utcnow(): tokendata = self.refreshandsavetokendata() preparedrequest.headers["Authorization"] = f"Bearer {tokendata["token"]}" return prepared_request ```