From 4cdea8f2ee495b35fe55303f8e102934498ae3b2 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 3 Oct 2025 15:35:09 +0200 Subject: [PATCH 1/2] Improve canonicalize_exception_traceback for RecursionError --- logfire/_internal/utils.py | 13 +++++- tests/test_canonicalize_exception.py | 63 ++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/logfire/_internal/utils.py b/logfire/_internal/utils.py index 2e421d0c0..ec280ae21 100644 --- a/logfire/_internal/utils.py +++ b/logfire/_internal/utils.py @@ -449,6 +449,7 @@ def canonicalize_exception_traceback(exc: BaseException, seen: set[int] | None = try: exc_type = type(exc) parts = [f'\n{exc_type.__module__}.{exc_type.__qualname__}\n----'] + num_repeats = 0 if exc.__traceback__: visited: set[str] = set() for frame, lineno in traceback.walk_tb(exc.__traceback__): @@ -456,7 +457,17 @@ def canonicalize_exception_traceback(exc: BaseException, seen: set[int] | None = source_line = linecache.getline(filename, lineno, frame.f_globals).strip() module = frame.f_globals.get('__name__', filename) frame_summary = f'{module}.{frame.f_code.co_name}\n {source_line}' - if frame_summary not in visited: # ignore repeated frames + if frame_summary in visited: + num_repeats += 1 + if num_repeats >= 100 and isinstance(exc, RecursionError): + # The last few frames of a RecursionError traceback are often *not* the recursive function(s) + # being called repeatedly (which are already deduped here) but instead some other function(s) + # called normally which happen to use up the last bit of the recursion limit. + # These can easily vary between runs and we don't want to pay attention to them, + # the real problem is the recursion itself. + parts.append('\n\n') + break + else: # skip repeated frames visited.add(frame_summary) parts.append(frame_summary) seen = seen or set() diff --git a/tests/test_canonicalize_exception.py b/tests/test_canonicalize_exception.py index 2c2f4ef86..c005e1e75 100644 --- a/tests/test_canonicalize_exception.py +++ b/tests/test_canonicalize_exception.py @@ -1,4 +1,5 @@ import sys +from typing import Callable import pytest from inline_snapshot import snapshot @@ -255,3 +256,65 @@ def test_canonicalize_no_traceback(): builtins.ValueError ----\ """) + + +def test_recursion(): + def foo(b: Callable[[], None]): + b() + foo(b) + + def foo2(): + foo(foo2) + + def bar(): + baz() + + def baz(): + pass + + try: + foo(bar) + except Exception as e: + assert canonicalize_exception_traceback(e).replace(__file__, '__file__') == snapshot("""\ + +builtins.RecursionError +---- +tests.test_canonicalize_exception.test_recursion + foo(bar) +tests.test_canonicalize_exception.foo + foo(b) + + +""") + + try: + foo(baz) + except Exception as e: + assert canonicalize_exception_traceback(e).replace(__file__, '__file__') == snapshot("""\ + +builtins.RecursionError +---- +tests.test_canonicalize_exception.test_recursion + foo(baz) +tests.test_canonicalize_exception.foo + foo(b) + + +""") + + try: + foo2() + except Exception as e: + assert canonicalize_exception_traceback(e).replace(__file__, '__file__') == snapshot("""\ + +builtins.RecursionError +---- +tests.test_canonicalize_exception.test_recursion + foo2() +tests.test_canonicalize_exception.foo2 + foo(foo2) +tests.test_canonicalize_exception.foo + b() + + +""") From 4027982b20ff7ae608a7a0e5c640ad50df94f185 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 3 Oct 2025 15:41:54 +0200 Subject: [PATCH 2/2] tweak --- logfire/_internal/utils.py | 2 +- tests/test_canonicalize_exception.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/logfire/_internal/utils.py b/logfire/_internal/utils.py index ec280ae21..3905cfcb3 100644 --- a/logfire/_internal/utils.py +++ b/logfire/_internal/utils.py @@ -465,7 +465,7 @@ def canonicalize_exception_traceback(exc: BaseException, seen: set[int] | None = # called normally which happen to use up the last bit of the recursion limit. # These can easily vary between runs and we don't want to pay attention to them, # the real problem is the recursion itself. - parts.append('\n\n') + parts.append('\n') break else: # skip repeated frames visited.add(frame_summary) diff --git a/tests/test_canonicalize_exception.py b/tests/test_canonicalize_exception.py index c005e1e75..6d6bad3f0 100644 --- a/tests/test_canonicalize_exception.py +++ b/tests/test_canonicalize_exception.py @@ -284,7 +284,7 @@ def baz(): tests.test_canonicalize_exception.foo foo(b) - +\ """) try: @@ -299,7 +299,7 @@ def baz(): tests.test_canonicalize_exception.foo foo(b) - +\ """) try: @@ -316,5 +316,5 @@ def baz(): tests.test_canonicalize_exception.foo b() - +\ """)