Skip to content

Commit 2daacc7

Browse files
authored
chore: improve error handling in frontend (baserow#5109)
1 parent d6c8c8b commit 2daacc7

39 files changed

Lines changed: 565 additions & 192 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ DATABASE_NAME=baserow
182182

183183
# BASEROW_TOTP_ISSUER_NAME=
184184

185+
# Set SENTRY_DSN to `fake` if you want to test sentry logging without sentry itself
185186
# SENTRY_DSN=
186187
# SENTRY_BACKEND_DSN=
187188

backend/src/baserow/config/settings/base.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,10 +1439,12 @@ def __setitem__(self, key, value):
14391439
if SENTRY_DSN:
14401440
import sentry_sdk
14411441
import sentry_sdk.integrations as _sentry_integrations
1442+
from loguru import logger
14421443
from sentry_sdk.integrations.django import DjangoIntegration
14431444
from sentry_sdk.scrubber import DEFAULT_DENYLIST, EventScrubber
14441445

14451446
from baserow.core.sentry import (
1447+
ConsoleSentryTransport,
14461448
drop_expected_asyncio_websocket_ping_timeout_events,
14471449
)
14481450

@@ -1457,6 +1459,15 @@ def __setitem__(self, key, value):
14571459
]
14581460

14591461
SENTRY_DENYLIST = DEFAULT_DENYLIST + ["username", "email", "name"]
1462+
sentry_transport = None
1463+
1464+
if SENTRY_DSN == "fake":
1465+
logger.info(
1466+
"[SENTRY] Using fake backend Sentry DSN, events will be logged to the "
1467+
"console."
1468+
)
1469+
SENTRY_DSN = "https://public@example.invalid/1"
1470+
sentry_transport = ConsoleSentryTransport()
14601471

14611472
sentry_sdk.init(
14621473
dsn=SENTRY_DSN,
@@ -1465,6 +1476,7 @@ def __setitem__(self, key, value):
14651476
before_send=drop_expected_asyncio_websocket_ping_timeout_events,
14661477
event_scrubber=EventScrubber(recursive=True, denylist=SENTRY_DENYLIST),
14671478
environment=os.getenv("SENTRY_ENVIRONMENT", ""),
1479+
transport=sentry_transport,
14681480
)
14691481
else:
14701482
BASEROW_LAZY_LOADED_LIBRARIES.append("sentry_sdk")

backend/src/baserow/core/sentry.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,67 @@
1+
import json
12
import logging
23
from typing import Any
34

45
from django.contrib.auth import get_user_model
56

7+
from loguru import logger
8+
from sentry_sdk.transport import Transport
9+
10+
SENTRY_LOG_PREFIX = "[SENTRY]"
11+
12+
13+
def _get_sentry_event_message(event: dict[str, Any]) -> str:
14+
exception_values = event.get("exception", {}).get("values", [])
15+
if exception_values:
16+
exception = exception_values[-1]
17+
exception_type = exception.get("type", "UnknownError")
18+
exception_value = exception.get("value", "")
19+
if exception_value:
20+
return f"{exception_type}: {exception_value}"
21+
return exception_type
22+
23+
log_entry = event.get("logentry", {})
24+
if log_entry.get("formatted"):
25+
return log_entry["formatted"]
26+
if log_entry.get("message"):
27+
return log_entry["message"]
28+
if event.get("message"):
29+
return str(event["message"])
30+
31+
return "Sentry event captured without a message."
32+
33+
34+
def log_sentry_event_to_console(event: dict[str, Any]) -> None:
35+
level = str(event.get("level", "error")).upper()
36+
event_id = event.get("event_id", "unknown")
37+
message = _get_sentry_event_message(event)
38+
logger.error(f"{SENTRY_LOG_PREFIX} [{level}] [{event_id}] {message}")
39+
40+
41+
class ConsoleSentryTransport(Transport):
42+
"""
43+
A transport that logs Sentry events locally instead of sending them to Sentry.
44+
"""
45+
46+
def capture_event(self, event):
47+
log_sentry_event_to_console(event)
48+
49+
def capture_envelope(self, envelope):
50+
for item in envelope.items:
51+
item_type = item.headers.get("type", "unknown")
52+
payload = item.get_bytes().decode("utf-8", errors="replace")
53+
54+
if item_type == "event":
55+
try:
56+
log_sentry_event_to_console(json.loads(payload))
57+
continue
58+
except json.JSONDecodeError:
59+
pass
60+
61+
logger.error(
62+
f"{SENTRY_LOG_PREFIX} [ENVELOPE] [{item_type.upper()}] {payload}"
63+
)
64+
665

766
def drop_expected_asyncio_websocket_ping_timeout_events(
867
event: dict[str, Any], hint: dict[str, Any]

backend/tests/baserow/core/test_sentry.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import logging
2+
from unittest.mock import patch
23

3-
from baserow.core.sentry import drop_expected_asyncio_websocket_ping_timeout_events
4+
from sentry_sdk.envelope import Envelope
5+
6+
from baserow.core.sentry import (
7+
ConsoleSentryTransport,
8+
drop_expected_asyncio_websocket_ping_timeout_events,
9+
log_sentry_event_to_console,
10+
)
411

512

613
def test_drop_expected_asyncio_websocket_ping_timeout_events():
@@ -44,3 +51,37 @@ def test_drop_expected_asyncio_websocket_ping_timeout_events_keeps_other_errors(
4451
)
4552
== event
4653
)
54+
55+
56+
@patch("baserow.core.sentry.logger")
57+
def test_log_sentry_event_to_console_logs_exception_with_prefix(mock_logger):
58+
log_sentry_event_to_console(
59+
{
60+
"event_id": "event-123",
61+
"level": "error",
62+
"exception": {
63+
"values": [
64+
{
65+
"type": "ValueError",
66+
"value": "broken",
67+
}
68+
]
69+
},
70+
}
71+
)
72+
73+
mock_logger.error.assert_called_once_with(
74+
"[SENTRY] [ERROR] [event-123] ValueError: broken"
75+
)
76+
77+
78+
@patch("baserow.core.sentry.logger")
79+
def test_console_sentry_transport_logs_envelope_payload(mock_logger):
80+
envelope = Envelope(headers={"event_id": "event-123"})
81+
envelope.add_event({"event_id": "event-123", "message": "Envelope event"})
82+
83+
ConsoleSentryTransport().capture_envelope(envelope)
84+
85+
mock_logger.error.assert_called_once_with(
86+
"[SENTRY] [ERROR] [event-123] Envelope event"
87+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "refactor",
3+
"message": "Improve error handling in frontend",
4+
"issue_origin": "github",
5+
"issue_number": null,
6+
"domain": "core",
7+
"bullet_points": [],
8+
"created_at": "2026-04-16"
9+
}

enterprise/web-frontend/modules/baserow_enterprise/pages/admin/dataScanner.vue

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,21 @@ if (!$hasFeature(EnterpriseFeatures.DATA_SCANNER)) {
4444
throw createError({
4545
statusCode: 401,
4646
message: 'Available in the enterprise version',
47+
data: {
48+
report: false,
49+
},
50+
fatal: true,
4751
})
4852
}
4953
5054
if (!store.getters['auth/isStaff']) {
51-
throw createError({ statusCode: 403, message: 'Forbidden.' })
55+
throw createError({
56+
statusCode: 403,
57+
message: 'Forbidden.',
58+
data: {
59+
report: false,
60+
},
61+
fatal: true,
62+
})
5263
}
5364
</script>

enterprise/web-frontend/modules/baserow_enterprise/pages/auditLog.vue

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,10 @@ if (workspaceId) {
161161
throw createError({
162162
statusCode: 401,
163163
message: 'Available in the advanced/enterprise version',
164+
data: {
165+
report: false,
166+
},
167+
fatal: true,
164168
})
165169
} else if (
166170
!$hasPermission(
@@ -169,15 +173,33 @@ if (workspaceId) {
169173
workspaceId
170174
)
171175
) {
172-
throw createError({ statusCode: 404, message: 'Page not found' })
176+
throw createError({
177+
statusCode: 404,
178+
message: 'Page not found',
179+
data: {
180+
report: false,
181+
},
182+
fatal: true,
183+
})
173184
}
174185
} else if (!$hasFeature(EnterpriseFeatures.AUDIT_LOG)) {
175186
throw createError({
176187
statusCode: 401,
177188
message: 'Available in the advanced/enterprise version',
189+
data: {
190+
report: false,
191+
},
192+
fatal: true,
178193
})
179194
} else if (!store.getters['auth/isStaff']) {
180-
throw createError({ statusCode: 403, message: 'Forbidden.' })
195+
throw createError({
196+
statusCode: 403,
197+
message: 'Forbidden.',
198+
data: {
199+
report: false,
200+
},
201+
fatal: true,
202+
})
181203
}
182204
183205
// Template refs

premium/web-frontend/modules/baserow_premium/pages/admin/license.vue

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,10 +224,23 @@ const { data, error } = await useAsyncData(
224224
route.params.id
225225
)
226226
return licenseData
227-
} catch {
227+
} catch (e) {
228+
const statusCode = e.response?.status || 500
229+
230+
if (statusCode === 404) {
231+
throw createError({
232+
statusCode: 404,
233+
message: 'The license was not found.',
234+
data: {
235+
report: false,
236+
},
237+
fatal: true,
238+
})
239+
}
228240
throw createError({
229-
statusCode: 404,
230-
message: 'The license was not found.',
241+
statusCode: statusCode,
242+
message: 'Something went wrong while fetching the licenses.',
243+
fatal: true,
231244
})
232245
}
233246
}

premium/web-frontend/modules/baserow_premium/pages/admin/licenses.vue

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,20 @@ const { data, error } = await useAsyncData('licensesPage', async () => {
181181
instanceId: instanceData.instance_id,
182182
}
183183
} catch (e) {
184+
const statusCode = e.response?.status || 500
185+
186+
if (statusCode === 404) {
187+
throw createError({
188+
statusCode: 404,
189+
message: 'Licenses not found.',
190+
data: {
191+
report: false,
192+
},
193+
fatal: true,
194+
})
195+
}
184196
throw createError({
185-
statusCode: 400,
197+
statusCode: statusCode,
186198
message: 'Something went wrong while fetching the licenses.',
187199
fatal: true,
188200
})

web-frontend/modules/automation/middleware.js

Whitespace-only changes.

0 commit comments

Comments
 (0)