Skip to content

Logging an exception before re-raising it turns a crash into a handled error #8096

Open

Description

@4ktLuffy

How do you use Sentry?

Sentry Saas (sentry.io)

Version

2.72.0 (Flask 3.1.3, Python 3.9). Same code path on master and major/3.0.

Steps to Reproduce

A common pattern: log the exception, then re-raise it.

# python repro.py <plain|log-then-raise>
import gzip, json, logging, sys, threading
from http.server import BaseHTTPRequestHandler, HTTPServer

import sentry_sdk
from flask import Flask

sessions, events = [], []


class Sink(BaseHTTPRequestHandler):
    def log_message(self, *a):
        pass

    def do_POST(self):
        body = self.rfile.read(int(self.headers["Content-Length"]))
        if self.headers.get("Content-Encoding") == "gzip":
            body = gzip.decompress(body)
        lines = body.split(b"\n")
        for i in range(1, len(lines) - 1, 2):
            try:
                item = json.loads(lines[i])
                payload = json.loads(lines[i + 1])
            except Exception:
                continue
            if item.get("type") == "sessions":
                sessions.extend(payload["aggregates"])
            if item.get("type") == "event":
                values = payload["exception"]["values"]
                events.append(values[-1]["mechanism"]["handled"])
        self.send_response(200)
        self.end_headers()


srv = HTTPServer(("127.0.0.1", 0), Sink)
threading.Thread(target=srv.serve_forever, daemon=True).start()
sentry_sdk.init(dsn="http://k@127.0.0.1:%d/1" % srv.server_port, release="demo@1.0.0")

app = Flask(__name__)
log = logging.getLogger("app")
mode = sys.argv[1]


@app.route("/")
def index():
    try:
        raise ValueError("db down")
    except ValueError:
        if mode == "log-then-raise":
            log.exception("failed to load")
        raise


logging.getLogger("werkzeug").disabled = True
app.logger.disabled = True
app.test_client().get("/")
sentry_sdk.get_client().session_flusher.flush()
sentry_sdk.flush()
totals = {k: sum(a.get(k, 0) for a in sessions) for k in ("exited", "errored", "crashed")}
print(f"{mode:15} events handled={events}  request sessions={totals}")

pip install sentry-sdk flask, then python repro.py plain and python repro.py log-then-raise.

Expected Result

Both requests fail with a 500, so both should count as crashed, and the event should keep handled: false.

Actual Result

plain           events handled=[False]  request sessions={'exited': 0, 'errored': 0, 'crashed': 1}
log-then-raise  events handled=[True]  request sessions={'exited': 0, 'errored': 1, 'crashed': 0}

With log.exception() first, the logging event (handled) goes out and updates the session to errored. The unhandled event from Flask is the same exception object, so DedupeIntegration drops it before the session update in capture_event. The crash never reaches the session or the issue.

The spec says dropped events shouldn't update the session because the developer chose to ignore them, but that isn't the case here.

Activity

  1. linear-code commented on Oct 21, 2026

    @linear-code
  2. moved this to Waiting for: Product Owner in GitHub Issues with 👀 3on Oct 21, 2026
  3. simpleqt commented on Nov 1, 2026

    @simpleqt

    Diagnosis + proposed direction (happy to open a draft PR once there's maintainer interest):

    sentry_sdk/integrations/dedupe.py keys _last_seen on the exception object alone, so when the same exception instance is first captured as handled (e.g. capture_exception in an except block) and later re-raised and reported as unhandled by the crash processor, the second event is dropped as a duplicate. The unhandled (crash) report never reaches Sentry and the session exits errored (1 error) instead of crashed (2).

    Proposed fix: key dedupe on (exception, unhandled) where unhandled mirrors the exact check Client._update_session_from_event uses (any mechanism.handled is False, AnnotatedValue-aware). Duplicates then collapse only when both identity AND handledness match, so the handled report and the later crash report both survive; true duplicates (same exception, same handledness) still collapse — no behavior change for the common path.

    If that direction sounds right I'll open a draft PR with tests covering both the log-then-raise pattern and the same-handledness dedupe guard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions