Skip to content

Commit 6351964

Browse files
committed
feat: use repr in code variables
1 parent 65785b8 commit 6351964

File tree

2 files changed

+99
-12
lines changed

2 files changed

+99
-12
lines changed

posthog/exception_utils.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -969,19 +969,30 @@ def _serialize_variable_value(value, limiter, max_length=1024):
969969
return result
970970
except Exception:
971971
try:
972-
fallback = f"<{type(value).__name__}>"
973-
fallback_size = len(fallback)
974-
if not limiter.can_add(fallback_size):
972+
result = repr(value)
973+
if len(result) > max_length:
974+
result = result[: max_length - 3] + "..."
975+
976+
result_size = len(result)
977+
if not limiter.can_add(result_size):
975978
return None
976-
limiter.add(fallback_size)
977-
return fallback
979+
limiter.add(result_size)
980+
return result
978981
except Exception:
979-
fallback = "<unserializable object>"
980-
fallback_size = len(fallback)
981-
if not limiter.can_add(fallback_size):
982-
return None
983-
limiter.add(fallback_size)
984-
return fallback
982+
try:
983+
fallback = f"<{type(value).__name__}>"
984+
fallback_size = len(fallback)
985+
if not limiter.can_add(fallback_size):
986+
return None
987+
limiter.add(fallback_size)
988+
return fallback
989+
except Exception:
990+
fallback = "<unserializable object>"
991+
fallback_size = len(fallback)
992+
if not limiter.can_add(fallback_size):
993+
return None
994+
limiter.add(fallback_size)
995+
return fallback
985996

986997

987998
def _is_simple_type(value):

posthog/test/test_exception_capture.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ def process_data():
9696
assert b"'my_number': 42" in output
9797
assert b"'my_bool': 'True'" in output
9898
assert b'"my_dict": "{\\"name\\": \\"test\\", \\"value\\": 123}"' in output
99-
assert b'"my_obj": "<UnserializableObject>"' in output
99+
# With repr() fallback, objects without custom __repr__ show full repr including module and memory address
100+
assert b"<__main__.UnserializableObject object at" in output
100101
assert b"'my_password': '$$_posthog_redacted_based_on_masking_rules_$$'" in output
101102
assert b"'__should_be_ignored':" not in output
102103

@@ -332,3 +333,78 @@ def process_data():
332333
assert '"code_variables":' not in output
333334
assert "'my_var'" not in output
334335
assert "'important_value'" not in output
336+
337+
338+
def test_code_variables_repr_fallback(tmpdir):
339+
"""Test that repr() is used for variables that can't be JSON-serialized but can be repr'd"""
340+
app = tmpdir.join("app.py")
341+
app.write(
342+
dedent(
343+
"""
344+
import os
345+
import re
346+
from datetime import datetime, timedelta
347+
from decimal import Decimal
348+
from fractions import Fraction
349+
from posthog import Posthog
350+
351+
class CustomReprClass:
352+
def __repr__(self):
353+
return '<CustomReprClass: custom representation>'
354+
355+
posthog = Posthog(
356+
'phc_x',
357+
host='https://eu.i.posthog.com',
358+
debug=True,
359+
enable_exception_autocapture=True,
360+
capture_exception_code_variables=True,
361+
project_root=os.path.dirname(os.path.abspath(__file__))
362+
)
363+
364+
def trigger_error():
365+
my_regex = re.compile(r'\\d+')
366+
my_datetime = datetime(2024, 1, 15, 10, 30, 45)
367+
my_timedelta = timedelta(days=5, hours=3)
368+
my_decimal = Decimal('123.456')
369+
my_fraction = Fraction(3, 4)
370+
my_set = {1, 2, 3}
371+
my_frozenset = frozenset([4, 5, 6])
372+
my_bytes = b'hello bytes'
373+
my_bytearray = bytearray(b'mutable bytes')
374+
my_memoryview = memoryview(b'memory view')
375+
my_complex = complex(3, 4)
376+
my_range = range(10)
377+
my_custom = CustomReprClass()
378+
my_lambda = lambda x: x * 2
379+
my_function = trigger_error
380+
381+
1/0
382+
383+
trigger_error()
384+
"""
385+
)
386+
)
387+
388+
with pytest.raises(subprocess.CalledProcessError) as excinfo:
389+
subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
390+
391+
output = excinfo.value.output.decode("utf-8")
392+
393+
assert "ZeroDivisionError" in output
394+
assert "code_variables" in output
395+
396+
assert "re.compile(" in output and "\\\\d+" in output
397+
assert "datetime.datetime(2024, 1, 15, 10, 30, 45)" in output
398+
assert "datetime.timedelta(days=5, seconds=10800)" in output
399+
assert "Decimal('123.456')" in output
400+
assert "Fraction(3, 4)" in output
401+
assert "{1, 2, 3}" in output
402+
assert "frozenset({4, 5, 6})" in output
403+
assert "b'hello bytes'" in output
404+
assert "bytearray(b'mutable bytes')" in output
405+
assert "<memory at" in output
406+
assert "(3+4j)" in output
407+
assert "range(0, 10)" in output
408+
assert "<CustomReprClass: custom representation>" in output
409+
assert "<lambda>" in output
410+
assert "<function trigger_error at" in output

0 commit comments

Comments
 (0)