From 4af9b167e5fb6a9f5216a940854be8b99079ac79 Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Mon, 9 Mar 2026 10:37:30 -0400 Subject: [PATCH 1/3] fix(seer): Add auth signing to grouping record delete-by-hash requests call_seer_to_delete_these_hashes() was making unsigned POST requests to Seer's delete-by-hash endpoint, bypassing the auth middleware and generating "No auth header found" log noise. Use make_signed_seer_api_request() to add Rpcsignature auth headers, matching how all other Seer calls work. Co-Authored-By: Claude --- src/sentry/seer/similarity/grouping_records.py | 14 +++++++------- .../seer/similarity/test_grouping_records.py | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/sentry/seer/similarity/grouping_records.py b/src/sentry/seer/similarity/grouping_records.py index f8e36118943283..e3d52cdfd4b5c1 100644 --- a/src/sentry/seer/similarity/grouping_records.py +++ b/src/sentry/seer/similarity/grouping_records.py @@ -5,7 +5,7 @@ from typing import TypedDict from django.conf import settings -from urllib3.exceptions import ReadTimeoutError +from urllib3.exceptions import MaxRetryError, ReadTimeoutError, TimeoutError from sentry import options from sentry.conf.server import ( @@ -13,6 +13,7 @@ SEER_PROJECT_GROUPING_RECORDS_DELETE_URL, ) from sentry.net.http import connection_from_url +from sentry.seer.signed_seer_api import make_signed_seer_api_request from sentry.utils import json, metrics logger = logging.getLogger(__name__) @@ -75,15 +76,14 @@ def call_seer_to_delete_project_grouping_records( def call_seer_to_delete_these_hashes(project_id: int, hashes: Sequence[str]) -> bool: extra = {"project_id": project_id, "hashes": hashes} try: - body = {"project_id": project_id, "hash_list": hashes} - response = seer_grouping_connection_pool.urlopen( - "POST", + body = json.dumps({"project_id": project_id, "hash_list": hashes}).encode("utf-8") + response = make_signed_seer_api_request( + seer_grouping_connection_pool, SEER_HASH_GROUPING_RECORDS_DELETE_URL, - body=json.dumps(body), - headers={"Content-Type": "application/json;charset=utf-8"}, + body=body, timeout=POST_BULK_GROUPING_RECORDS_TIMEOUT, ) - except ReadTimeoutError: + except (ReadTimeoutError, TimeoutError, MaxRetryError): extra.update({"reason": "ReadTimeoutError", "timeout": POST_BULK_GROUPING_RECORDS_TIMEOUT}) logger.exception( "seer.delete_grouping_records.hashes.timeout", diff --git a/tests/sentry/seer/similarity/test_grouping_records.py b/tests/sentry/seer/similarity/test_grouping_records.py index 229b880a75e6a3..ba70905f047a7a 100644 --- a/tests/sentry/seer/similarity/test_grouping_records.py +++ b/tests/sentry/seer/similarity/test_grouping_records.py @@ -18,7 +18,7 @@ @django_db_all @mock.patch("sentry.seer.similarity.grouping_records.logger") -@mock.patch("sentry.seer.similarity.grouping_records.seer_grouping_connection_pool.urlopen") +@mock.patch("sentry.seer.similarity.grouping_records.make_signed_seer_api_request") def test_delete_grouping_records_by_hash_success( mock_seer_request: MagicMock, mock_logger: MagicMock ): @@ -40,7 +40,7 @@ def test_delete_grouping_records_by_hash_success( @django_db_all @mock.patch("sentry.seer.similarity.grouping_records.logger") -@mock.patch("sentry.seer.similarity.grouping_records.seer_grouping_connection_pool.urlopen") +@mock.patch("sentry.seer.similarity.grouping_records.make_signed_seer_api_request") def test_delete_grouping_records_by_hash_timeout( mock_seer_request: MagicMock, mock_logger: MagicMock ): @@ -63,7 +63,7 @@ def test_delete_grouping_records_by_hash_timeout( @django_db_all @mock.patch("sentry.seer.similarity.grouping_records.logger") -@mock.patch("sentry.seer.similarity.grouping_records.seer_grouping_connection_pool.urlopen") +@mock.patch("sentry.seer.similarity.grouping_records.make_signed_seer_api_request") def test_delete_grouping_records_by_hash_failure( mock_seer_request: MagicMock, mock_logger: MagicMock ): From a59ee483c406075e511676822461f9b338ca7a33 Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Mon, 9 Mar 2026 10:43:04 -0400 Subject: [PATCH 2/3] fix(seer): Use actual exception type in error reason instead of hardcoded string Use type(e).__name__ for the reason in logs and metrics so that MaxRetryError and TimeoutError are correctly identified rather than all being labeled as ReadTimeoutError. Co-Authored-By: Claude --- src/sentry/seer/similarity/grouping_records.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sentry/seer/similarity/grouping_records.py b/src/sentry/seer/similarity/grouping_records.py index e3d52cdfd4b5c1..f5db5bb2b9b3b3 100644 --- a/src/sentry/seer/similarity/grouping_records.py +++ b/src/sentry/seer/similarity/grouping_records.py @@ -83,8 +83,8 @@ def call_seer_to_delete_these_hashes(project_id: int, hashes: Sequence[str]) -> body=body, timeout=POST_BULK_GROUPING_RECORDS_TIMEOUT, ) - except (ReadTimeoutError, TimeoutError, MaxRetryError): - extra.update({"reason": "ReadTimeoutError", "timeout": POST_BULK_GROUPING_RECORDS_TIMEOUT}) + except (ReadTimeoutError, TimeoutError, MaxRetryError) as e: + extra.update({"reason": type(e).__name__, "timeout": POST_BULK_GROUPING_RECORDS_TIMEOUT}) logger.exception( "seer.delete_grouping_records.hashes.timeout", extra=extra, @@ -92,7 +92,7 @@ def call_seer_to_delete_these_hashes(project_id: int, hashes: Sequence[str]) -> metrics.incr( DELETE_HASH_METRIC, sample_rate=options.get("seer.similarity.metrics_sample_rate"), - tags={"success": False, "reason": "ReadTimeoutError"}, + tags={"success": False, "reason": type(e).__name__}, ) return False From a92580e8523bd1f8c51197313b5a5a33264d3605 Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Mon, 9 Mar 2026 10:48:07 -0400 Subject: [PATCH 3/3] ref(seer): Remove redundant ReadTimeoutError from exception tuple ReadTimeoutError is a subclass of TimeoutError, so catching both is redundant. Match the pattern used in similar_issues.py. The import is retained for the other function in this module. Co-Authored-By: Claude --- src/sentry/seer/similarity/grouping_records.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/seer/similarity/grouping_records.py b/src/sentry/seer/similarity/grouping_records.py index f5db5bb2b9b3b3..d45f17bb8b9a63 100644 --- a/src/sentry/seer/similarity/grouping_records.py +++ b/src/sentry/seer/similarity/grouping_records.py @@ -83,7 +83,7 @@ def call_seer_to_delete_these_hashes(project_id: int, hashes: Sequence[str]) -> body=body, timeout=POST_BULK_GROUPING_RECORDS_TIMEOUT, ) - except (ReadTimeoutError, TimeoutError, MaxRetryError) as e: + except (TimeoutError, MaxRetryError) as e: extra.update({"reason": type(e).__name__, "timeout": POST_BULK_GROUPING_RECORDS_TIMEOUT}) logger.exception( "seer.delete_grouping_records.hashes.timeout",