<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><id>https://blog.diba.dev/en</id><title>blog.diba.dev</title><updated>2026-06-20T14:43:42.974521+00:00</updated><link href="https://blog.diba.dev/en" rel="alternate"/><link href="https://blog.diba.dev/en/feed.xml" rel="self"/><generator uri="https://lkiesow.github.io/python-feedgen" version="1.0.0">python-feedgen</generator><subtitle>Technical blog about Python, Flask and other things.</subtitle><entry><id>https://blog.diba.dev/en/posts/hello-world</id><title>Hello World</title><updated>2026-02-21T00:00:00+01:00</updated><content type="html">&lt;p&gt;Welcome to the blog! This is the first post, serving as a formatting showcase.&lt;/p&gt;
&lt;h2 id="why-flask"&gt;Why Flask?&lt;/h2&gt;
&lt;p&gt;Flask is a minimalist web framework for Python. Unlike Django, it doesn't dictate your project structure — you build exactly what you need.&lt;/p&gt;
&lt;h2 id="code-example"&gt;Code example&lt;/h2&gt;
&lt;p&gt;A simple Flask application:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;flask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Hello, World!&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And here's an example of working with files in Python:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pathlib&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;posts_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;posts&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;md_file&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;posts_dir&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*.md&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Found post: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;md_file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="lists"&gt;Lists&lt;/h2&gt;
&lt;p&gt;Things this blog can do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Markdown posts with YAML frontmatter&lt;/li&gt;
&lt;li&gt;Syntax highlighting via Pygments&lt;/li&gt;
&lt;li&gt;Lazy cache — HTML is generated on first access&lt;/li&gt;
&lt;li&gt;RSS/Atom feed&lt;/li&gt;
&lt;li&gt;Tags and filtering by tags&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="inline-code"&gt;Inline code&lt;/h2&gt;
&lt;p&gt;Configuration can be found in &lt;code&gt;app.py&lt;/code&gt;. Posts are stored in the &lt;code&gt;posts/&lt;/code&gt; directory as &lt;code&gt;.md&lt;/code&gt; files.&lt;/p&gt;</content><link href="https://blog.diba.dev/en/posts/hello-world"/><summary>&lt;p&gt;First blog post. A showcase of syntax, code and formatting.&lt;/p&gt;</summary><published>2026-02-21T00:00:00+01:00</published></entry><entry><id>https://blog.diba.dev/en/posts/sentry-exception-group-title</id><title>More descriptive titles for Sentry events</title><updated>2026-02-22T00:00:00+01:00</updated><content type="html">&lt;p&gt;How to turn a Sentry issue title from &lt;code&gt;ExceptionGroup: Download failed&lt;/code&gt; into &lt;code&gt;DownloadError -&amp;gt; [HTTPDownloadError -&amp;gt; RuntimeError, HTTPStatusError]&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The problem&lt;/h2&gt;
&lt;p&gt;Sentry displays issues with &lt;code&gt;exception_type&lt;/code&gt; as the title and &lt;code&gt;exception_value&lt;/code&gt; as the subtitle. With &lt;code&gt;ExceptionGroup&lt;/code&gt;, this leads to identical titles for completely different root causes — the title is always just &lt;code&gt;ExceptionGroup&lt;/code&gt; and the subtitle a generic message:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ExceptionGroup: Download failed
ExceptionGroup: Download failed
ExceptionGroup: Download failed
ExceptionGroup: Download failed
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Four different Sentry issues, each with a different problem (DNS error, HTTP 403, HTTP 404, invalid URL), but indistinguishable at first glance.&lt;/p&gt;
&lt;p&gt;The same problem occurs with exceptions wrapped in a generic envelope. This is a common pattern — the wrapper carries meta-information (retry count, endpoint URL, batch ID), but Sentry only shows the wrapper type:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;DownloadError: Download failed
DownloadError: Download failed
DownloadError: Download failed
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The actual causes are different every time — &lt;code&gt;ConnectionTimeout&lt;/code&gt;, &lt;code&gt;SSLCertVerificationError&lt;/code&gt;, &lt;code&gt;HTTPStatusError(403)&lt;/code&gt; — but in the issues list we only see a generic &lt;code&gt;DownloadError&lt;/code&gt;. We have to click into each event to find out what actually happened.&lt;/p&gt;
&lt;p&gt;Or for example:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;TaskError: Task processing failed
TaskError: Task processing failed
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;One is a &lt;code&gt;JSONDecodeError&lt;/code&gt;, the other a &lt;code&gt;PermissionError&lt;/code&gt; — but the title is identical.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The core of the problem:&lt;/strong&gt; Both &lt;code&gt;ExceptionGroup&lt;/code&gt; and exceptions with chains (&lt;code&gt;__cause__&lt;/code&gt;/&lt;code&gt;__context__&lt;/code&gt;) carry the real information in nested exceptions, not in the top-level message. Sentry cannot display this structure in the title.&lt;/p&gt;
&lt;h2 id="solution-sdk-tag-server-side-fingerprint-rule"&gt;Solution: SDK tag + server-side fingerprint rule&lt;/h2&gt;
&lt;p&gt;A combination of two mechanisms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SDK&lt;/strong&gt; (Python, &lt;code&gt;before_send&lt;/code&gt; hook) — has access to the live exception object, can recursively traverse &lt;code&gt;ExceptionGroup.exceptions&lt;/code&gt; and &lt;code&gt;__cause__&lt;/code&gt;/&lt;code&gt;__context__&lt;/code&gt; chains. Writes the result as a tag.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sentry server&lt;/strong&gt; (fingerprint rule) — can override the title, but needs ready-made data. The tag from the SDK is available in the event.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="why-it-works"&gt;Why it works&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;before_send&lt;/code&gt; is called before sending the event to the server — the tag is part of the payload&lt;/li&gt;
&lt;li&gt;Server-side fingerprint rules are applied after receiving the event — they see tags from the SDK&lt;/li&gt;
&lt;li&gt;&lt;code&gt;title=&lt;/code&gt; in a fingerprint rule is the only documented way to override an issue title&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="why-a-tag-and-not-another-mechanism"&gt;Why a tag and not another mechanism&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A tag is a standard part of the Event payload — no hacking of &lt;code&gt;TypedDict&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;It's visible in the Sentry UI (filtering, searching)&lt;/li&gt;
&lt;li&gt;The fingerprint rule &lt;code&gt;tags.*&lt;/code&gt; matcher is documented and supported&lt;/li&gt;
&lt;li&gt;Clean separation: the backend provides data, the server displays it&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="implementation"&gt;Implementation&lt;/h2&gt;
&lt;h3 id="exception-mixin"&gt;Exception mixin&lt;/h3&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;SentryTitleMixin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Mixin for exceptions with informative Sentry titles.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="nd"&gt;@staticmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_format_sentry_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="ne"&gt;BaseException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BaseExceptionGroup&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;inner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;, &amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;SentryTitleMixin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_format_sentry_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; [&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]&amp;quot;&lt;/span&gt;
        &lt;span class="n"&gt;cause&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__cause__&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__context__&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SentryTitleMixin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_format_sentry_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;sentry_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Return a human-readable title describing the exception chain.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ne"&gt;BaseException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_format_sentry_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;_format_sentry_title()&lt;/code&gt; is a &lt;code&gt;@staticmethod&lt;/code&gt; because it recursively traverses nested exceptions that don't have &lt;code&gt;SentryTitleMixin&lt;/code&gt; themselves — it needs to accept any &lt;code&gt;BaseException&lt;/code&gt;, not just &lt;code&gt;self&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The recursive &lt;code&gt;_format_sentry_title()&lt;/code&gt; builds a hierarchical description:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ExceptionGroup&lt;/strong&gt; → &lt;code&gt;Name -&amp;gt; [Sub1, Sub2]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chained exception&lt;/strong&gt; (&lt;code&gt;__cause__&lt;/code&gt;/&lt;code&gt;__context__&lt;/code&gt;) → &lt;code&gt;Name -&amp;gt; CauseName&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Combination&lt;/strong&gt; → &lt;code&gt;DownloadError -&amp;gt; [HTTPDownloadError -&amp;gt; RuntimeError, HTTPStatusError]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="exception-with-mixin"&gt;Exception with mixin&lt;/h3&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;DownloadError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ExceptionGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SentryTitleMixin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Download failed in all attempts.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;No &lt;code&gt;__new__&lt;/code&gt;, no &lt;code&gt;derive()&lt;/code&gt; — the mixin is opt-in and only added to exceptions where it makes sense. It only affects Sentry display, no changes to exception behavior.&lt;/p&gt;
&lt;h3 id="before_send-hook"&gt;&lt;code&gt;before_send&lt;/code&gt; hook&lt;/h3&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;before_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# ... existing filters ...&lt;/span&gt;

    &lt;span class="c1"&gt;# SentryTitleMixin — set tag for server-side fingerprint rules.&lt;/span&gt;
    &lt;span class="c1"&gt;#   Requires a fingerprint rule in Sentry (Project Settings → Issue Grouping):&lt;/span&gt;
    &lt;span class="c1"&gt;#   tags.sentry_title:* -&amp;gt; {{ default }} title=&amp;quot;{{ tags.sentry_title }}&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;exc_info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;exc_info&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exc_info&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc_info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;exc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exc_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SentryTitleMixin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tags&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;sentry_title&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sentry_title&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="fingerprint-rule-in-sentry-ui"&gt;Fingerprint rule in Sentry UI&lt;/h3&gt;
&lt;p&gt;Project Settings → Issue Grouping → Fingerprint Rules:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;tags.sentry_title:* -&amp;gt; {{ default }} title=&amp;quot;{{ tags.sentry_title }}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;{{ default }}&lt;/code&gt; preserves the default grouping — the rule only changes the displayed title.&lt;/p&gt;
&lt;h2 id="result"&gt;Result&lt;/h2&gt;
&lt;p&gt;Instead of:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ExceptionGroup: Download failed
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Sentry displays:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;DownloadError -&amp;gt; [HTTPDownloadError -&amp;gt; RuntimeError, HTTPStatusError]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="broader-observations"&gt;Broader observations&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The Sentry SDK and server have &lt;strong&gt;asymmetric capabilities&lt;/strong&gt;: the SDK sees runtime objects, the server only sees the serialized payload. But only the server can override the title (via fingerprint rules).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ExceptionGroup&lt;/code&gt; (Python 3.11+) is still a second-class citizen in Sentry — the title only shows the top-level message, not the sub-exception structure.&lt;/li&gt;
&lt;li&gt;Fingerprint rules are primarily designed for grouping, but the &lt;code&gt;title=&lt;/code&gt; attribute makes them the only way to affect how an issue appears in the list.&lt;/li&gt;
&lt;li&gt;The pattern &lt;strong&gt;"SDK prepares data as a tag, server consumes it in a rule"&lt;/strong&gt; is generally applicable to other scenarios where you need runtime information in server-side configuration.&lt;/li&gt;
&lt;/ul&gt;</content><link href="https://blog.diba.dev/en/posts/sentry-exception-group-title"/><summary>&lt;p&gt;How to turn a Sentry issue title from &lt;code&gt;ExceptionGroup: Download failed&lt;/code&gt; into &lt;code&gt;DownloadError -&amp;gt; [HTTPDownloadError -&amp;gt; RuntimeError, HTTPStatusError]&lt;/code&gt;.&lt;/p&gt;</summary><published>2026-02-22T00:00:00+01:00</published></entry></feed>