Skip to content

feat(browser_reporting): Only set headers if option is available #93641

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

Merged
merged 5 commits into from
Jun 18, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions src/sentry/middleware/reporting_endpoint.py
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@
from django.http.request import HttpRequest
from django.http.response import HttpResponseBase

from sentry import options


class ReportingEndpointMiddleware:
"""
@@ -14,17 +16,16 @@ def __init__(self, get_response: Callable[[HttpRequest], HttpResponseBase]):

def __call__(self, request: HttpRequest) -> HttpResponseBase:
response = self.get_response(request)
return self.process_response(request, response)

def process_response(
self, request: HttpRequest, response: HttpResponseBase
) -> HttpResponseBase:
# Check if the request has staff attribute and if staff is active
staff = getattr(request, "staff", None)
if staff and staff.is_active:
# This will enable crashes, intervention and deprecation warnings
# They always report to the default endpoint
response["Reporting-Endpoints"] = (
"default=https://sentry.my.sentry.io/api/0/reporting-api-experiment/"
)

try:
enabled = options.get("issues.browser_reporting.reporting_endpoints_header_enabled")
if enabled:
# This will enable crashes, intervention and deprecation warnings
# They always report to the default endpoint
response["Reporting-Endpoints"] = (
"default=https://sentry.my.sentry.io/api/0/reporting-api-experiment/"
)
except Exception:
pass

return response
9 changes: 9 additions & 0 deletions src/sentry/options/defaults.py
Original file line number Diff line number Diff line change
@@ -3467,6 +3467,15 @@
flags=FLAG_ALLOW_EMPTY | FLAG_AUTOMATOR_MODIFIABLE,
)

# Enable adding the `Reporting-Endpoints` header, which will in turn enable the sending of Reporting
# API reports from the browser (as long as it's Chrome).
register(
"issues.browser_reporting.reporting_endpoints_header_enabled",
type=Bool,
default=False,
flags=FLAG_AUTOMATOR_MODIFIABLE,
)

# Enable the collection of Reporting API reports via the `/api/0/reporting-api-experiment/`
# endpoint. When this is false, the endpoint will just 404.
register(
8 changes: 7 additions & 1 deletion tests/sentry/api/endpoints/test_relay_register.py
Original file line number Diff line number Diff line change
@@ -564,5 +564,11 @@ def test_no_db_for_static_relays(self):
static_auth = {relay_id: {"internal": True, "public_key": str(public_key)}}

with self.assertNumQueries(0):
with override_options({"relay.static_auth": static_auth}):
with override_options(
{
"relay.static_auth": static_auth,
# XXX: Temporary; remove it once the endpoint is removed
"issues.browser_reporting.reporting_endpoints_header_enabled": False,
}
):
self.register_relay(key_pair, "1.1.1", relay_id)
95 changes: 38 additions & 57 deletions tests/sentry/middleware/test_reporting_endpoint.py
Original file line number Diff line number Diff line change
@@ -1,68 +1,49 @@
from unittest.mock import Mock
from unittest.mock import patch

from django.http import HttpResponse, HttpResponseBase
from django.http import HttpResponse
from django.test import RequestFactory

from sentry.middleware.reporting_endpoint import ReportingEndpointMiddleware
from sentry.testutils.cases import TestCase
from sentry.testutils.helpers.options import override_options


class ReportingEndpointMiddlewareTestCase(TestCase):
def setUp(self) -> None:
self.middleware = ReportingEndpointMiddleware(lambda request: HttpResponse())
self.factory = RequestFactory()

def _no_header_set(self, result: HttpResponseBase) -> None:
assert "Reporting-Endpoints" not in result

def test_adds_header_for_staff_user(self) -> None:
"""Test that the ReportingEndpoint header is added when user is Sentry staff."""
request = self.factory.get("/")

# Mock staff object with is_active = True
staff_mock = Mock()
staff_mock.is_active = True
setattr(request, "staff", staff_mock)

response = HttpResponse()
result = self.middleware.process_response(request, response)

assert "Reporting-Endpoints" in result
assert (
result["Reporting-Endpoints"]
== "default=https://sentry.my.sentry.io/api/0/reporting-api-experiment/"
)

def test_no_header_for_non_staff_user(self) -> None:
"""Test that the ReportingEndpoint header is not added when user is not Sentry staff."""
request = self.factory.get("/")

# Mock staff object with is_active = False
staff_mock = Mock()
staff_mock.is_active = False
setattr(request, "staff", staff_mock)

response = HttpResponse()
result = self.middleware.process_response(request, response)

self._no_header_set(result)

def test_no_header_when_no_staff_attribute(self) -> None:
"""Test that the ReportingEndpoint header is not added when request has no staff attribute."""
request = self.factory.get("/")

# No staff attribute on request
response = HttpResponse()
result = self.middleware.process_response(request, response)

self._no_header_set(result)

def test_no_header_when_staff_is_none(self) -> None:
"""Test that the ReportingEndpoint header is not added when staff is None."""
request = self.factory.get("/")
setattr(request, "staff", None)

response = HttpResponse()
result = self.middleware.process_response(request, response)

self._no_header_set(result)
def test_obeys_option_when_enabled(self) -> None:
with override_options(
{"issues.browser_reporting.reporting_endpoints_header_enabled": True}
):
# Create middleware with option enabled
middleware = ReportingEndpointMiddleware(lambda request: HttpResponse())
request = self.factory.get("/")
response = middleware(request)

assert (
response.get("Reporting-Endpoints")
== "default=https://sentry.my.sentry.io/api/0/reporting-api-experiment/"
)

def test_obeys_option_when_disabled(self) -> None:
with override_options(
{"issues.browser_reporting.reporting_endpoints_header_enabled": False}
):
# Create middleware with option disabled
middleware = ReportingEndpointMiddleware(lambda _: HttpResponse())
request = self.factory.get("/")
response = middleware(request)

assert response.get("Reporting-Endpoints") is None

def test_handles_option_fetch_failure(self) -> None:
with patch("sentry.middleware.reporting_endpoint.options.get") as mock_options_get:
mock_options_get.side_effect = Exception("Database error")

# Should handle the exception gracefully and default to disabled
middleware = ReportingEndpointMiddleware(lambda _: HttpResponse())
request = self.factory.get("/")
response = middleware(request)

assert response.get("Reporting-Endpoints") is None