diff --git a/backend/src/baserow/config/settings/base.py b/backend/src/baserow/config/settings/base.py index 64356a0632..8d63049d44 100644 --- a/backend/src/baserow/config/settings/base.py +++ b/backend/src/baserow/config/settings/base.py @@ -1435,6 +1435,10 @@ def __setitem__(self, key, value): from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.scrubber import DEFAULT_DENYLIST, EventScrubber + from baserow.core.sentry import ( + drop_expected_asyncio_websocket_ping_timeout_events, + ) + # Exclude integrations whose module-level imports are incompatible: # - pydantic_ai: sentry-sdk patches ToolManager._call_tool which was # removed in pydantic-ai >= 1.x (now execute_tool_call) @@ -1451,6 +1455,7 @@ def __setitem__(self, key, value): dsn=SENTRY_DSN, integrations=[DjangoIntegration(signals_spans=False, middleware_spans=False)], send_default_pii=False, + before_send=drop_expected_asyncio_websocket_ping_timeout_events, event_scrubber=EventScrubber(recursive=True, denylist=SENTRY_DENYLIST), environment=os.getenv("SENTRY_ENVIRONMENT", ""), ) diff --git a/backend/src/baserow/core/sentry.py b/backend/src/baserow/core/sentry.py index 7b35bde147..2d4bda4b41 100644 --- a/backend/src/baserow/core/sentry.py +++ b/backend/src/baserow/core/sentry.py @@ -1,6 +1,37 @@ +import logging +from typing import Any + from django.contrib.auth import get_user_model +def drop_expected_asyncio_websocket_ping_timeout_events( + event: dict[str, Any], hint: dict[str, Any] +) -> dict[str, Any] | None: + """ + Ignore websocket keepalive timeouts logged by asyncio. + + These are emitted by the websockets stack when a client disappears without a + clean close handshake. They are noisy, expected in production, and don't + point to an application error in Baserow itself. + """ + + log_record = hint.get("log_record") + if not isinstance(log_record, logging.LogRecord): + return event + + if log_record.name != "asyncio": + return event + + message = log_record.getMessage() + if ( + "ConnectionClosedError exception in shielded future" in message + and "keepalive ping timeout" in message + ): + return None + + return event + + def setup_user_in_sentry(user): """ This function sets the user in the Sentry context. This is useful for debugging diff --git a/backend/tests/baserow/core/test_sentry.py b/backend/tests/baserow/core/test_sentry.py new file mode 100644 index 0000000000..18769933d4 --- /dev/null +++ b/backend/tests/baserow/core/test_sentry.py @@ -0,0 +1,46 @@ +import logging + +from baserow.core.sentry import drop_expected_asyncio_websocket_ping_timeout_events + + +def test_drop_expected_asyncio_websocket_ping_timeout_events(): + event = {"logger": "asyncio"} + record = logging.makeLogRecord( + { + "name": "asyncio", + "levelno": logging.ERROR, + "levelname": "ERROR", + "msg": ( + "ConnectionClosedError exception in shielded future future: " + ", reason='keepalive " + "ping timeout'), None)>" + ), + } + ) + + assert ( + drop_expected_asyncio_websocket_ping_timeout_events( + event, {"log_record": record} + ) + is None + ) + + +def test_drop_expected_asyncio_websocket_ping_timeout_events_keeps_other_errors(): + event = {"logger": "asyncio"} + record = logging.makeLogRecord( + { + "name": "asyncio", + "levelno": logging.ERROR, + "levelname": "ERROR", + "msg": "Task exception was never retrieved", + } + ) + + assert ( + drop_expected_asyncio_websocket_ping_timeout_events( + event, {"log_record": record} + ) + == event + ) diff --git a/changelog/entries/unreleased/bug/5224_suppress_expected_websocket_keepalive_timeout_sentry_noise.json b/changelog/entries/unreleased/bug/5224_suppress_expected_websocket_keepalive_timeout_sentry_noise.json new file mode 100644 index 0000000000..86e90d86d8 --- /dev/null +++ b/changelog/entries/unreleased/bug/5224_suppress_expected_websocket_keepalive_timeout_sentry_noise.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Prevent expected websocket keepalive timeout logs from being reported to Sentry.", + "issue_origin": "github", + "issue_number": 5224, + "domain": "core", + "bullet_points": [], + "created_at": "2026-04-17" +}