Skip to content
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
5 changes: 5 additions & 0 deletions backend/src/baserow/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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", ""),
)
Expand Down
31 changes: 31 additions & 0 deletions backend/src/baserow/core/sentry.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
46 changes: 46 additions & 0 deletions backend/tests/baserow/core/test_sentry.py
Original file line number Diff line number Diff line change
@@ -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: "
"<Future finished exception=ConnectionClosedError(None, "
"Close(code=<CloseCode.INTERNAL_ERROR: 1011>, 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
)
Original file line number Diff line number Diff line change
@@ -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"
}
Loading