Compare commits

...
Author SHA1 Message Date
Raphael Michel 96a4b3d7f6 Navigation and dashboard: Do not crash page if a plugin fails
Generally, I don't think it's a good approach for plugins to fail
silently. A missing menu item or button is a weird failure mode, people
will search for it and the system will feel buggy. However, a single bug
in a plugin should not cause the entire system to feel "down" since the
entire backend can't load, not even the dashboard right after login. So
here, I think the trade-off is warranted to fail silently -- and
escalate quickly via Sentry.
2026-11-03 17:12:23 +01:00
3 changed files with 44 additions and 9 deletions

No files matched your search

+38 -3
View File
@@ -147,7 +147,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
def _is_receiver_active(self, sender, receiver):
return is_receiver_active(sender, receiver)
def send(self, sender: T, **named) -> List[Tuple[Callable, Any]]:
def send(self, sender: T, skip_exceptions=False, **named) -> List[Tuple[Callable, Any]]:
"""
Send signal from sender to all connected receivers that belong to
plugins enabled for the given event / organizer.
@@ -164,8 +164,19 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
for receiver in self._live_receivers(sender)[0]:
if self._is_receiver_active(sender, receiver):
response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response))
try:
response = receiver(signal=self, sender=sender, **named)
except Exception as e:
if skip_exceptions:
logger.exception("Signal handler skipped due to exception")
if settings.SENTRY_ENABLED:
import sentry_sdk
sentry_sdk.capture_exception(e)
else:
raise
else:
responses.append((receiver, response))
return responses
def send_chained(self, sender: T, chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
@@ -312,6 +323,30 @@ class OrganizerPluginSignal(PluginSignal[Organizer]):
class GlobalSignal(django.dispatch.Signal):
def send(self, sender, skip_exceptions=False, **named):
if (
not self.receivers
or self.sender_receivers_cache.get(sender) is NO_RECEIVERS
):
return []
responses = []
sync_receivers, _ = self._live_receivers(sender)
for receiver in sync_receivers:
try:
response = receiver(signal=self, sender=sender, **named)
except Exception as e:
if skip_exceptions:
logger.exception("Signal handler skipped due to exception")
if settings.SENTRY_ENABLED:
import sentry_sdk
sentry_sdk.capture_exception(e)
else:
raise
else:
responses.append((receiver, response))
return responses
def send_chained(self, sender: Event, chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
"""
Send signal from sender to all connected receivers. The return value of the first receiver
+3 -3
View File
@@ -135,7 +135,7 @@ def get_event_navigation(request: HttpRequest):
# It would be better to allow plugins to handle the permission themselves, but for backwards compatibility
# we need to have it in the "if" statement
event_settings += sorted(
sum((list(a[1]) for a in nav_event_settings.send(request.event, request=request)), []),
sum((list(a[1]) for a in nav_event_settings.send(request.event, skip_exceptions=True, request=request)), []),
key=lambda r: r['label']
)
if event_settings:
@@ -487,7 +487,7 @@ def get_global_navigation(request):
})
merge_in(nav, sorted(
sum((list(a[1]) for a in nav_global.send(request, request=request)), []),
sum((list(a[1]) for a in nav_global.send(request, skip_exceptions=True, request=request)), []),
key=lambda r: (1 if r.get('parent') else 0, r['label'])
))
return nav
@@ -718,7 +718,7 @@ def get_organizer_navigation(request):
})
merge_in(nav, sorted(
sum((list(a[1]) for a in nav_organizer.send(request.organizer, request=request, organizer=request.organizer)),
sum((list(a[1]) for a in nav_organizer.send(request.organizer, skip_exceptions=True, request=request, organizer=request.organizer)),
[]),
key=lambda r: (1 if r.get('parent') else 0, r['label'])
))
+3 -3
View File
@@ -370,7 +370,7 @@ def event_index(request, organizer, event):
widgets = []
if can_view_orders:
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=True):
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=True, skip_exceptions=True):
widgets.extend(result)
ctx = {
@@ -406,7 +406,7 @@ def event_index_widgets_lazy(request, organizer, event):
request=request)
widgets = []
if can_view_orders:
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False):
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False, skip_exceptions=True):
widgets.extend(result)
return build_json_response(widgets)
@@ -666,7 +666,7 @@ def user_index_widgets_lazy(request):
def user_index(request):
widgets = []
for r, result in user_dashboard_widgets.send(request, user=request.user):
for r, result in user_dashboard_widgets.send(request, skip_exceptions=True, user=request.user):
widgets.extend(result)
ctx = {