Skip to content

fix(browser-reports): Support the correct media type #93814

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 1 commit into from
Jun 18, 2025
Merged
Show file tree
Hide file tree
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
19 changes: 14 additions & 5 deletions src/sentry/issues/endpoints/browser_reporting_collector.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Any

from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
Expand All @@ -14,6 +15,15 @@
logger = logging.getLogger(__name__)


class BrowserReportsJSONParser(JSONParser):
"""
Custom parser for browser Reporting API that handles the application/reports+json content type.
This extends JSONParser since the content is still JSON, just with a different media type.
"""

media_type = "application/reports+json"


@all_silo_endpoint
class BrowserReportingCollectorEndpoint(Endpoint):
"""
Expand All @@ -23,18 +33,17 @@ class BrowserReportingCollectorEndpoint(Endpoint):
"""

permission_classes = ()
# TODO: Do we need to specify this parser? Content type will be `application/reports+json`, so
# it might just work automatically.
parser_classes = [JSONParser]
# Support both standard JSON and browser reporting API content types
parser_classes = [BrowserReportsJSONParser, JSONParser]
publish_status = {
"POST": ApiPublishStatus.PRIVATE,
}
owner = ApiOwner.ISSUES

# TODO: It's unclear if either of these decorators is necessary
# CSRF exemption and CORS support required for Browser Reporting API
@csrf_exempt
@allow_cors_options
def post(self, request: Request, *args, **kwargs) -> HttpResponse:
def post(self, request: Request, *args: Any, **kwargs: Any) -> HttpResponse:
if not options.get("issues.browser_reporting.collector_endpoint_enabled"):
return HttpResponse(status=404)

Expand Down
16 changes: 15 additions & 1 deletion tests/sentry/api/endpoints/test_browser_reporting_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ def test_404s_by_default(self):
def test_logs_request_data_if_option_enabled(
self, mock_logger_info: MagicMock, mock_metrics_incr: MagicMock
):
response = self.client.post(self.url, self.report_data)
response = self.client.post(
self.url, self.report_data, content_type="application/reports+json"
)

assert response.status_code == status.HTTP_200_OK
mock_logger_info.assert_any_call(
Expand All @@ -48,3 +50,15 @@ def test_logs_request_data_if_option_enabled(
mock_metrics_incr.assert_any_call(
"browser_reporting.raw_report_received", tags={"type": self.report_data["type"]}
)

@override_options({"issues.browser_reporting.collector_endpoint_enabled": True})
@patch("sentry.issues.endpoints.browser_reporting_collector.metrics.incr")
def test_rejects_invalid_content_type(self, mock_metrics_incr: MagicMock):
"""Test that the endpoint rejects invalid content type and does not call the browser reporting metric"""
response = self.client.post(self.url, self.report_data, content_type="bad/type/json")

assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
# Verify that the browser_reporting.raw_report_received metric was not called
# Check that none of the calls were for the browser_reporting.raw_report_received metric
for call in mock_metrics_incr.call_args_list:
assert call[0][0] != "browser_reporting.raw_report_received"
Loading