Jak proměnit titulek Sentry issue z ExceptionGroup: Download failed na DownloadError -> [HTTPDownloadError -> RuntimeError, HTTPStatusError].

Problém

Sentry zobrazuje issue tak, že titulkem je exception_type a podtitulkem exception_value. U ExceptionGroup to vede k identickým titulkům pro zcela odlišné root causes — titulek je vždy jen ExceptionGroup a podtitulek generický message:

ExceptionGroup: Download failed
ExceptionGroup: Download failed
ExceptionGroup: Download failed
ExceptionGroup: Download failed

Čtyři různé Sentry issues, každý s jiným problémem (DNS error, HTTP 403, HTTP 404, nevalidní URL), ale na první pohled nerozlišitelné. Stejný problém nastává u výjimek balených do generického obalu. To je běžný pattern — obal nese metainformaci (retry count, endpoint URL, batch ID), ale Sentry zobrazí jen typ obalu:

DownloadError: Download failed
DownloadError: Download failed
DownloadError: Download failed

Příčiny jsou přitom pokaždé jiné — ConnectionTimeout, SSLCertVerificationError, HTTPStatusError(403) — ale v seznamu issues vidíme jen generický DownloadError. Musíme rozkliknout každý event, abychom zjistili, co se skutečně stalo.

Nebo třeba:

TaskError: Task processing failed
TaskError: Task processing failed

Jeden je JSONDecodeError, druhý PermissionError — ale titulek je identický.

Jádro problému: Jak ExceptionGroup, tak výjimky s chain (__cause__/__context__) nesou skutečnou informaci ve vnořených výjimkách, ne v top-level message. Sentry tuhle strukturu neumí zobrazit v titulku.

Řešení: SDK tag + server-side fingerprint rule

Kombinace dvou mechanismů:

  • SDK (Python, before_send hook) — má přístup k živému exception objektu, umí rekurzivně projít ExceptionGroup.exceptions a __cause__/__context__ chainy. Výsledek zapíše jako tag.
  • Sentry server (fingerprint rule) — umí přepsat titulek, ale potřebuje hotová data. Tag z SDK je v eventu k dispozici.

Proč to funguje

  1. before_send se volá před odesláním eventu na server — tag je součástí payloadu
  2. Server-side fingerprint rules se aplikují po přijetí eventu — vidí tagy z SDK
  3. title= v fingerprint rule je jediný dokumentovaný způsob, jak přepsat titulek issue

Proč tag a ne jiný mechanismus

  • Tag je standardní součást Event payloadu — žádné hackování TypedDictu
  • Je viditelný v Sentry UI (filtrování, vyhledávání)
  • Fingerprint rule tags.* matcher je dokumentovaný a podporovaný
  • Čistá separace: backend dodá data, server je zobrazí

Implementace

Mixin pro výjimky

class SentryTitleMixin:
    """Mixin for exceptions with informative Sentry titles."""

    @staticmethod
    def _format_sentry_title(exc: BaseException) -> str:
        name = type(exc).__name__
        if isinstance(exc, BaseExceptionGroup):
            inner = ", ".join(
                SentryTitleMixin._format_sentry_title(e) for e in exc.exceptions
            )
            return f"{name} -> [{inner}]"
        cause = exc.__cause__ or exc.__context__
        if cause:
            return f"{name} -> {SentryTitleMixin._format_sentry_title(cause)}"
        return name

    def sentry_title(self) -> str:
        """Return a human-readable title describing the exception chain."""
        assert isinstance(self, BaseException)
        return self._format_sentry_title(self)

_format_sentry_title() je @staticmethod, protože rekurzivně prochází vnořené výjimky, které samy SentryTitleMixin nemají — potřebuje přijmout libovolný BaseException, ne jen self.

Rekurzivní _format_sentry_title() sestaví hierarchický popis:

  • ExceptionGroupName -> [Sub1, Sub2]
  • Chained exception (__cause__/__context__) → Name -> CauseName
  • KombinaceDownloadError -> [HTTPDownloadError -> RuntimeError, HTTPStatusError]

Výjimka s mixinem

class DownloadError(ExceptionGroup, SentryTitleMixin):
    """Download failed in all attempts."""

Žádný __new__, žádný derive() — mixin je opt-in a přidává se jen na výjimky, kde to dává smysl. Zasahuje pouze do Sentry zobrazení, žádné změny v chování výjimek.

before_send hook

def before_send(event, hint):
    # ... existing filters ...

    # SentryTitleMixin — set tag for server-side fingerprint rules.
    #   Requires a fingerprint rule in Sentry (Project Settings → Issue Grouping):
    #   tags.sentry_title:* -> {{ default }} title="{{ tags.sentry_title }}"
    exc_info = hint.get("exc_info") if hint else None
    if exc_info and len(exc_info) >= 2:
        exc = exc_info[1]
        if isinstance(exc, SentryTitleMixin):
            event.setdefault("tags", {})["sentry_title"] = exc.sentry_title()

    return event

Fingerprint rule v Sentry UI

Project Settings → Issue Grouping → Fingerprint Rules:

tags.sentry_title:* -> {{ default }} title="{{ tags.sentry_title }}"

{{ default }} zachovává výchozí grouping — pravidlo mění pouze zobrazený titulek.

Výsledek

Místo:

ExceptionGroup: Download failed

Sentry zobrazí:

DownloadError -> [HTTPDownloadError -> RuntimeError, HTTPStatusError]

Obecnější pozorování

  • Sentry SDK a server mají asymetrické schopnosti: SDK vidí runtime objekty, server vidí jen serializovaný payload. Ale titulek umí přepsat jen server (přes fingerprint rules).
  • ExceptionGroup (Python 3.11+) je v Sentry stále občan druhé kategorie — titulek zobrazuje jen top-level message, ne strukturu sub-výjimek.
  • Fingerprint rules jsou primárně navržené pro grouping, ale title= atribut z nich dělá jediný způsob, jak ovlivnit zobrazení issue v seznamu.
  • Pattern "SDK připraví data jako tag, server je spotřebuje v pravidle" je obecně použitelný i pro jiné scénáře, kde potřebujete runtime informaci v server-side konfiguraci.