From 635196428c5744a3e7baae4027ca020ab9a82fa9 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Sat, 15 Nov 2025 10:25:18 +0100 Subject: [PATCH 1/6] feat: use repr in code variables --- posthog/exception_utils.py | 33 +++++++---- posthog/test/test_exception_capture.py | 78 +++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 12 deletions(-) diff --git a/posthog/exception_utils.py b/posthog/exception_utils.py index ad6cd33d..f4bebf73 100644 --- a/posthog/exception_utils.py +++ b/posthog/exception_utils.py @@ -969,19 +969,30 @@ def _serialize_variable_value(value, limiter, max_length=1024): return result except Exception: try: - fallback = f"<{type(value).__name__}>" - fallback_size = len(fallback) - if not limiter.can_add(fallback_size): + result = repr(value) + if len(result) > max_length: + result = result[: max_length - 3] + "..." + + result_size = len(result) + if not limiter.can_add(result_size): return None - limiter.add(fallback_size) - return fallback + limiter.add(result_size) + return result except Exception: - fallback = "" - fallback_size = len(fallback) - if not limiter.can_add(fallback_size): - return None - limiter.add(fallback_size) - return fallback + try: + fallback = f"<{type(value).__name__}>" + fallback_size = len(fallback) + if not limiter.can_add(fallback_size): + return None + limiter.add(fallback_size) + return fallback + except Exception: + fallback = "" + fallback_size = len(fallback) + if not limiter.can_add(fallback_size): + return None + limiter.add(fallback_size) + return fallback def _is_simple_type(value): diff --git a/posthog/test/test_exception_capture.py b/posthog/test/test_exception_capture.py index 837a2b8e..84915b02 100644 --- a/posthog/test/test_exception_capture.py +++ b/posthog/test/test_exception_capture.py @@ -96,7 +96,8 @@ def process_data(): assert b"'my_number': 42" in output assert b"'my_bool': 'True'" in output assert b'"my_dict": "{\\"name\\": \\"test\\", \\"value\\": 123}"' in output - assert b'"my_obj": ""' in output + # With repr() fallback, objects without custom __repr__ show full repr including module and memory address + assert b"<__main__.UnserializableObject object at" in output assert b"'my_password': '$$_posthog_redacted_based_on_masking_rules_$$'" in output assert b"'__should_be_ignored':" not in output @@ -332,3 +333,78 @@ def process_data(): assert '"code_variables":' not in output assert "'my_var'" not in output assert "'important_value'" not in output + + +def test_code_variables_repr_fallback(tmpdir): + """Test that repr() is used for variables that can't be JSON-serialized but can be repr'd""" + app = tmpdir.join("app.py") + app.write( + dedent( + """ + import os + import re + from datetime import datetime, timedelta + from decimal import Decimal + from fractions import Fraction + from posthog import Posthog + + class CustomReprClass: + def __repr__(self): + return '' + + posthog = Posthog( + 'phc_x', + host='https://eu.i.posthog.com', + debug=True, + enable_exception_autocapture=True, + capture_exception_code_variables=True, + project_root=os.path.dirname(os.path.abspath(__file__)) + ) + + def trigger_error(): + my_regex = re.compile(r'\\d+') + my_datetime = datetime(2024, 1, 15, 10, 30, 45) + my_timedelta = timedelta(days=5, hours=3) + my_decimal = Decimal('123.456') + my_fraction = Fraction(3, 4) + my_set = {1, 2, 3} + my_frozenset = frozenset([4, 5, 6]) + my_bytes = b'hello bytes' + my_bytearray = bytearray(b'mutable bytes') + my_memoryview = memoryview(b'memory view') + my_complex = complex(3, 4) + my_range = range(10) + my_custom = CustomReprClass() + my_lambda = lambda x: x * 2 + my_function = trigger_error + + 1/0 + + trigger_error() + """ + ) + ) + + with pytest.raises(subprocess.CalledProcessError) as excinfo: + subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT) + + output = excinfo.value.output.decode("utf-8") + + assert "ZeroDivisionError" in output + assert "code_variables" in output + + assert "re.compile(" in output and "\\\\d+" in output + assert "datetime.datetime(2024, 1, 15, 10, 30, 45)" in output + assert "datetime.timedelta(days=5, seconds=10800)" in output + assert "Decimal('123.456')" in output + assert "Fraction(3, 4)" in output + assert "{1, 2, 3}" in output + assert "frozenset({4, 5, 6})" in output + assert "b'hello bytes'" in output + assert "bytearray(b'mutable bytes')" in output + assert "" in output + assert "" in output + assert " Date: Sat, 15 Nov 2025 13:09:16 +0100 Subject: [PATCH 2/6] fix: remove comments --- posthog/client.py | 10 +++++ posthog/test/test_exception_capture.py | 2 - test.py | 57 ++++++++++++++++++++++++++ test_output.txt | 33 +++++++++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 test.py create mode 100644 test_output.txt diff --git a/posthog/client.py b/posthog/client.py index 3c4d8b89..bef9357f 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -991,11 +991,21 @@ def capture_exception( context_mask = get_code_variables_mask_patterns_context() context_ignore = get_code_variables_ignore_patterns_context() + print("\n\n\nCAPTURING EXCEPTION\n\n\n") + enabled = ( context_enabled if context_enabled is not None else self.capture_exception_code_variables ) + + print("Context enabled state: ", context_enabled) + print( + "Self capture_exception_code_variables: ", + self.capture_exception_code_variables, + ) + print("Computed enabled state: ", enabled) + mask_patterns = ( context_mask if context_mask is not None diff --git a/posthog/test/test_exception_capture.py b/posthog/test/test_exception_capture.py index 84915b02..16ceaf91 100644 --- a/posthog/test/test_exception_capture.py +++ b/posthog/test/test_exception_capture.py @@ -96,7 +96,6 @@ def process_data(): assert b"'my_number': 42" in output assert b"'my_bool': 'True'" in output assert b'"my_dict": "{\\"name\\": \\"test\\", \\"value\\": 123}"' in output - # With repr() fallback, objects without custom __repr__ show full repr including module and memory address assert b"<__main__.UnserializableObject object at" in output assert b"'my_password': '$$_posthog_redacted_based_on_masking_rules_$$'" in output assert b"'__should_be_ignored':" not in output @@ -336,7 +335,6 @@ def process_data(): def test_code_variables_repr_fallback(tmpdir): - """Test that repr() is used for variables that can't be JSON-serialized but can be repr'd""" app = tmpdir.join("app.py") app.write( dedent( diff --git a/test.py b/test.py new file mode 100644 index 00000000..fcbf97a6 --- /dev/null +++ b/test.py @@ -0,0 +1,57 @@ +import os +import re +from datetime import datetime, timedelta +from decimal import Decimal +from fractions import Fraction +from posthog import Posthog + +class CustomReprClass: + def __repr__(self): + return '' + +class CustomObject: + def __init__(self, value): + self.value = value + + def __repr__(self): + return f'CustomObject(value={self.value})' + +class CircularRef: + def __init__(self): + self.ref = self + + def __repr__(self): + return '' + +posthog = Posthog( + "phc_J1o2BXYxzXBHJeG2mS5hk62ijkTWk38Z385lO0MhU5w", + host="http://localhost:8010", + debug=True, + enable_exception_autocapture=True, + capture_exception_code_variables=True, + project_root=os.path.dirname(os.path.abspath(__file__)) +) + +def trigger_error(): + # Variables that can't be JSON-serialized but have useful repr() + my_regex = re.compile(r'\d+') + my_datetime = datetime(2024, 1, 15, 10, 30, 45) + my_timedelta = timedelta(days=5, hours=3) + my_decimal = Decimal('123.456') + my_fraction = Fraction(3, 4) + my_set = {1, 2, 3} + my_frozenset = frozenset([4, 5, 6]) + my_bytes = b'hello bytes' + my_bytearray = bytearray(b'mutable bytes') + my_memoryview = memoryview(b'memory view') + my_complex = complex(3, 4) + my_range = range(10) + my_custom = CustomReprClass() + my_obj = CustomObject(42) + my_circular = CircularRef() + my_lambda = lambda x: x * 2 + my_function = trigger_error + + 1/0 + +trigger_error() diff --git a/test_output.txt b/test_output.txt new file mode 100644 index 00000000..3e487525 --- /dev/null +++ b/test_output.txt @@ -0,0 +1,33 @@ +DEBUG:posthog:consumer is running... +DEBUG:posthog:queueing: {'properties': {'$exception_type': 'ZeroDivisionError', '$exception_message': 'division by zero', '$exception_list': [{'mechanism': {'type': 'generic', 'handled': True}, 'module': None, 'type': 'ZeroDivisionError', 'value': 'division by zero', 'stacktrace': {'frames': [{'platform': 'python', 'filename': 'test.py', 'abs_path': '/Users/ablaszkiewicz/Documents/repos/posthog-python/test.py', 'function': '', 'module': '__main__', 'lineno': 57, 'pre_context': [' my_lambda = lambda x: x * 2', ' my_function = trigger_error', ' ', ' 1/0', ''], 'context_line': 'trigger_error()', 'post_context': [], 'in_app': True, 'code_variables': {'CircularRef': "", 'CustomObject': "", 'CustomReprClass': "", 'Decimal': "", 'Fraction': "", 'Posthog': "", 'datetime': "", 'os': "", 'posthog': '', 're': "", 'timedelta': "", 'trigger_error': ''}}, {'platform': 'python', 'filename': 'test.py', 'abs_path': '/Users/ablaszkiewicz/Documents/repos/posthog-python/test.py', 'function': 'trigger_error', 'module': '__main__', 'lineno': 55, 'pre_context': [' my_obj = CustomObject(42)', ' my_circular = CircularRef()', ' my_lambda = lambda x: x * 2', ' my_function = trigger_error', ' '], 'context_line': ' 1/0', 'post_context': ['', 'trigger_error()'], 'in_app': True, 'code_variables': {'my_bytearray': "bytearray(b'mutable bytes')", 'my_bytes': "b'hello bytes'", 'my_circular': '', 'my_complex': '(3+4j)', 'my_custom': '', 'my_datetime': 'datetime.datetime(2024, 1, 15, 10, 30, 45)', 'my_decimal': "Decimal('123.456')", 'my_fraction': 'Fraction(3, 4)', 'my_frozenset': 'frozenset({4, 5, 6})', 'my_function': '', 'my_lambda': '. at 0x1077a07c0>', 'my_memoryview': '', 'my_obj': 'CustomObject(value=42)', 'my_range': 'range(0, 10)', 'my_regex': "re.compile('\\\\d+')", 'my_set': '{1, 2, 3}', 'my_timedelta': 'datetime.timedelta(days=5, seconds=10800)'}}], 'type': 'raw'}}], '$python_runtime': 'CPython', '$python_version': '3.11.12', '$os': 'Mac OS X', '$os_version': '15.6.1', '$process_person_profile': False, '$lib': 'posthog-python', '$lib_version': '6.9.1', '$geoip_disable': True}, 'timestamp': '2025-11-15T09:17:48.228041+00:00', 'distinct_id': '0d6d2c5b-7dde-4066-a691-b005b13e8220', 'event': '$exception', 'uuid': 'd96626d6-a5b7-4d50-8e2a-0b957826aea5'} +DEBUG:posthog:enqueued $exception. +Traceback (most recent call last): + File "/Users/ablaszkiewicz/Documents/repos/posthog-python/test.py", line 57, in + trigger_error() + File "/Users/ablaszkiewicz/Documents/repos/posthog-python/test.py", line 55, in trigger_error + 1/0 + ~^~ +ZeroDivisionError: division by zero +DEBUG:posthog:making request: {"batch": [{"properties": {"$exception_type": "ZeroDivisionError", "$exception_message": "division by zero", "$exception_list": [{"mechanism": {"type": "generic", "handled": true}, "module": null, "type": "ZeroDivisionError", "value": "division by zero", "stacktrace": {"frames": [{"platform": "python", "filename": "test.py", "abs_path": "/Users/ablaszkiewicz/Documents/repos/posthog-python/test.py", "function": "", "module": "__main__", "lineno": 57, "pre_context": [" my_lambda = lambda x: x * 2", " my_function = trigger_error", " ", " 1/0", ""], "context_line": "trigger_error()", "post_context": [], "in_app": true, "code_variables": {"CircularRef": "", "CustomObject": "", "CustomReprClass": "", "Decimal": "", "Fraction": "", "Posthog": "", "datetime": "", "os": "", "posthog": "", "re": "", "timedelta": "", "trigger_error": ""}}, {"platform": "python", "filename": "test.py", "abs_path": "/Users/ablaszkiewicz/Documents/repos/posthog-python/test.py", "function": "trigger_error", "module": "__main__", "lineno": 55, "pre_context": [" my_obj = CustomObject(42)", " my_circular = CircularRef()", " my_lambda = lambda x: x * 2", " my_function = trigger_error", " "], "context_line": " 1/0", "post_context": ["", "trigger_error()"], "in_app": true, "code_variables": {"my_bytearray": "bytearray(b'mutable bytes')", "my_bytes": "b'hello bytes'", "my_circular": "", "my_complex": "(3+4j)", "my_custom": "", "my_datetime": "datetime.datetime(2024, 1, 15, 10, 30, 45)", "my_decimal": "Decimal('123.456')", "my_fraction": "Fraction(3, 4)", "my_frozenset": "frozenset({4, 5, 6})", "my_function": "", "my_lambda": ". at 0x1077a07c0>", "my_memoryview": "", "my_obj": "CustomObject(value=42)", "my_range": "range(0, 10)", "my_regex": "re.compile('\\\\d+')", "my_set": "{1, 2, 3}", "my_timedelta": "datetime.timedelta(days=5, seconds=10800)"}}], "type": "raw"}}], "$python_runtime": "CPython", "$python_version": "3.11.12", "$os": "Mac OS X", "$os_version": "15.6.1", "$process_person_profile": false, "$lib": "posthog-python", "$lib_version": "6.9.1", "$geoip_disable": true}, "timestamp": "2025-11-15T09:17:48.228041+00:00", "distinct_id": "0d6d2c5b-7dde-4066-a691-b005b13e8220", "event": "$exception", "uuid": "d96626d6-a5b7-4d50-8e2a-0b957826aea5"}], "historical_migration": false, "sentAt": "2025-11-15T09:17:48.730239+00:00", "api_key": "phc_J1o2BXYxzXBHJeG2mS5hk62ijkTWk38Z385lO0MhU5w"} to url: http://localhost:8010/batch/ +DEBUG:posthog:data uploaded successfully +DEBUG:posthog:data uploaded successfully +DEBUG:posthog:consumer exited. + + + +CAPTURING EXCEPTION + + + +Context enabled state: None +Self capture_exception_code_variables: True +Computed enabled state: True +Trying to attach code variables to frames +tb_frames found +exception found +variables found +variables: {'CircularRef': "", 'CustomObject': "", 'CustomReprClass': "", 'Decimal': "", 'Fraction': "", 'Posthog': "", 'datetime': "", 'os': "", 'posthog': '', 're': "", 'timedelta': "", 'trigger_error': ''} +code_variables added +variables found +variables: {'my_bytearray': "bytearray(b'mutable bytes')", 'my_bytes': "b'hello bytes'", 'my_circular': '', 'my_complex': '(3+4j)', 'my_custom': '', 'my_datetime': 'datetime.datetime(2024, 1, 15, 10, 30, 45)', 'my_decimal': "Decimal('123.456')", 'my_fraction': 'Fraction(3, 4)', 'my_frozenset': 'frozenset({4, 5, 6})', 'my_function': '', 'my_lambda': '. at 0x1077a07c0>', 'my_memoryview': '', 'my_obj': 'CustomObject(value=42)', 'my_range': 'range(0, 10)', 'my_regex': "re.compile('\\\\d+')", 'my_set': '{1, 2, 3}', 'my_timedelta': 'datetime.timedelta(days=5, seconds=10800)'} +code_variables added From 920f7e8363fb5787af363ace469ae6ff73997f78 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Sat, 15 Nov 2025 13:10:24 +0100 Subject: [PATCH 3/6] Revert "fix: remove comments" This reverts commit a1ace330d16e00d5a4147b9f1aaf10b3a0022129. --- posthog/client.py | 10 ----- posthog/test/test_exception_capture.py | 2 + test.py | 57 -------------------------- test_output.txt | 33 --------------- 4 files changed, 2 insertions(+), 100 deletions(-) delete mode 100644 test.py delete mode 100644 test_output.txt diff --git a/posthog/client.py b/posthog/client.py index bef9357f..3c4d8b89 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -991,21 +991,11 @@ def capture_exception( context_mask = get_code_variables_mask_patterns_context() context_ignore = get_code_variables_ignore_patterns_context() - print("\n\n\nCAPTURING EXCEPTION\n\n\n") - enabled = ( context_enabled if context_enabled is not None else self.capture_exception_code_variables ) - - print("Context enabled state: ", context_enabled) - print( - "Self capture_exception_code_variables: ", - self.capture_exception_code_variables, - ) - print("Computed enabled state: ", enabled) - mask_patterns = ( context_mask if context_mask is not None diff --git a/posthog/test/test_exception_capture.py b/posthog/test/test_exception_capture.py index 16ceaf91..84915b02 100644 --- a/posthog/test/test_exception_capture.py +++ b/posthog/test/test_exception_capture.py @@ -96,6 +96,7 @@ def process_data(): assert b"'my_number': 42" in output assert b"'my_bool': 'True'" in output assert b'"my_dict": "{\\"name\\": \\"test\\", \\"value\\": 123}"' in output + # With repr() fallback, objects without custom __repr__ show full repr including module and memory address assert b"<__main__.UnserializableObject object at" in output assert b"'my_password': '$$_posthog_redacted_based_on_masking_rules_$$'" in output assert b"'__should_be_ignored':" not in output @@ -335,6 +336,7 @@ def process_data(): def test_code_variables_repr_fallback(tmpdir): + """Test that repr() is used for variables that can't be JSON-serialized but can be repr'd""" app = tmpdir.join("app.py") app.write( dedent( diff --git a/test.py b/test.py deleted file mode 100644 index fcbf97a6..00000000 --- a/test.py +++ /dev/null @@ -1,57 +0,0 @@ -import os -import re -from datetime import datetime, timedelta -from decimal import Decimal -from fractions import Fraction -from posthog import Posthog - -class CustomReprClass: - def __repr__(self): - return '' - -class CustomObject: - def __init__(self, value): - self.value = value - - def __repr__(self): - return f'CustomObject(value={self.value})' - -class CircularRef: - def __init__(self): - self.ref = self - - def __repr__(self): - return '' - -posthog = Posthog( - "phc_J1o2BXYxzXBHJeG2mS5hk62ijkTWk38Z385lO0MhU5w", - host="http://localhost:8010", - debug=True, - enable_exception_autocapture=True, - capture_exception_code_variables=True, - project_root=os.path.dirname(os.path.abspath(__file__)) -) - -def trigger_error(): - # Variables that can't be JSON-serialized but have useful repr() - my_regex = re.compile(r'\d+') - my_datetime = datetime(2024, 1, 15, 10, 30, 45) - my_timedelta = timedelta(days=5, hours=3) - my_decimal = Decimal('123.456') - my_fraction = Fraction(3, 4) - my_set = {1, 2, 3} - my_frozenset = frozenset([4, 5, 6]) - my_bytes = b'hello bytes' - my_bytearray = bytearray(b'mutable bytes') - my_memoryview = memoryview(b'memory view') - my_complex = complex(3, 4) - my_range = range(10) - my_custom = CustomReprClass() - my_obj = CustomObject(42) - my_circular = CircularRef() - my_lambda = lambda x: x * 2 - my_function = trigger_error - - 1/0 - -trigger_error() diff --git a/test_output.txt b/test_output.txt deleted file mode 100644 index 3e487525..00000000 --- a/test_output.txt +++ /dev/null @@ -1,33 +0,0 @@ -DEBUG:posthog:consumer is running... -DEBUG:posthog:queueing: {'properties': {'$exception_type': 'ZeroDivisionError', '$exception_message': 'division by zero', '$exception_list': [{'mechanism': {'type': 'generic', 'handled': True}, 'module': None, 'type': 'ZeroDivisionError', 'value': 'division by zero', 'stacktrace': {'frames': [{'platform': 'python', 'filename': 'test.py', 'abs_path': '/Users/ablaszkiewicz/Documents/repos/posthog-python/test.py', 'function': '', 'module': '__main__', 'lineno': 57, 'pre_context': [' my_lambda = lambda x: x * 2', ' my_function = trigger_error', ' ', ' 1/0', ''], 'context_line': 'trigger_error()', 'post_context': [], 'in_app': True, 'code_variables': {'CircularRef': "", 'CustomObject': "", 'CustomReprClass': "", 'Decimal': "", 'Fraction': "", 'Posthog': "", 'datetime': "", 'os': "", 'posthog': '', 're': "", 'timedelta': "", 'trigger_error': ''}}, {'platform': 'python', 'filename': 'test.py', 'abs_path': '/Users/ablaszkiewicz/Documents/repos/posthog-python/test.py', 'function': 'trigger_error', 'module': '__main__', 'lineno': 55, 'pre_context': [' my_obj = CustomObject(42)', ' my_circular = CircularRef()', ' my_lambda = lambda x: x * 2', ' my_function = trigger_error', ' '], 'context_line': ' 1/0', 'post_context': ['', 'trigger_error()'], 'in_app': True, 'code_variables': {'my_bytearray': "bytearray(b'mutable bytes')", 'my_bytes': "b'hello bytes'", 'my_circular': '', 'my_complex': '(3+4j)', 'my_custom': '', 'my_datetime': 'datetime.datetime(2024, 1, 15, 10, 30, 45)', 'my_decimal': "Decimal('123.456')", 'my_fraction': 'Fraction(3, 4)', 'my_frozenset': 'frozenset({4, 5, 6})', 'my_function': '', 'my_lambda': '. at 0x1077a07c0>', 'my_memoryview': '', 'my_obj': 'CustomObject(value=42)', 'my_range': 'range(0, 10)', 'my_regex': "re.compile('\\\\d+')", 'my_set': '{1, 2, 3}', 'my_timedelta': 'datetime.timedelta(days=5, seconds=10800)'}}], 'type': 'raw'}}], '$python_runtime': 'CPython', '$python_version': '3.11.12', '$os': 'Mac OS X', '$os_version': '15.6.1', '$process_person_profile': False, '$lib': 'posthog-python', '$lib_version': '6.9.1', '$geoip_disable': True}, 'timestamp': '2025-11-15T09:17:48.228041+00:00', 'distinct_id': '0d6d2c5b-7dde-4066-a691-b005b13e8220', 'event': '$exception', 'uuid': 'd96626d6-a5b7-4d50-8e2a-0b957826aea5'} -DEBUG:posthog:enqueued $exception. -Traceback (most recent call last): - File "/Users/ablaszkiewicz/Documents/repos/posthog-python/test.py", line 57, in - trigger_error() - File "/Users/ablaszkiewicz/Documents/repos/posthog-python/test.py", line 55, in trigger_error - 1/0 - ~^~ -ZeroDivisionError: division by zero -DEBUG:posthog:making request: {"batch": [{"properties": {"$exception_type": "ZeroDivisionError", "$exception_message": "division by zero", "$exception_list": [{"mechanism": {"type": "generic", "handled": true}, "module": null, "type": "ZeroDivisionError", "value": "division by zero", "stacktrace": {"frames": [{"platform": "python", "filename": "test.py", "abs_path": "/Users/ablaszkiewicz/Documents/repos/posthog-python/test.py", "function": "", "module": "__main__", "lineno": 57, "pre_context": [" my_lambda = lambda x: x * 2", " my_function = trigger_error", " ", " 1/0", ""], "context_line": "trigger_error()", "post_context": [], "in_app": true, "code_variables": {"CircularRef": "", "CustomObject": "", "CustomReprClass": "", "Decimal": "", "Fraction": "", "Posthog": "", "datetime": "", "os": "", "posthog": "", "re": "", "timedelta": "", "trigger_error": ""}}, {"platform": "python", "filename": "test.py", "abs_path": "/Users/ablaszkiewicz/Documents/repos/posthog-python/test.py", "function": "trigger_error", "module": "__main__", "lineno": 55, "pre_context": [" my_obj = CustomObject(42)", " my_circular = CircularRef()", " my_lambda = lambda x: x * 2", " my_function = trigger_error", " "], "context_line": " 1/0", "post_context": ["", "trigger_error()"], "in_app": true, "code_variables": {"my_bytearray": "bytearray(b'mutable bytes')", "my_bytes": "b'hello bytes'", "my_circular": "", "my_complex": "(3+4j)", "my_custom": "", "my_datetime": "datetime.datetime(2024, 1, 15, 10, 30, 45)", "my_decimal": "Decimal('123.456')", "my_fraction": "Fraction(3, 4)", "my_frozenset": "frozenset({4, 5, 6})", "my_function": "", "my_lambda": ". at 0x1077a07c0>", "my_memoryview": "", "my_obj": "CustomObject(value=42)", "my_range": "range(0, 10)", "my_regex": "re.compile('\\\\d+')", "my_set": "{1, 2, 3}", "my_timedelta": "datetime.timedelta(days=5, seconds=10800)"}}], "type": "raw"}}], "$python_runtime": "CPython", "$python_version": "3.11.12", "$os": "Mac OS X", "$os_version": "15.6.1", "$process_person_profile": false, "$lib": "posthog-python", "$lib_version": "6.9.1", "$geoip_disable": true}, "timestamp": "2025-11-15T09:17:48.228041+00:00", "distinct_id": "0d6d2c5b-7dde-4066-a691-b005b13e8220", "event": "$exception", "uuid": "d96626d6-a5b7-4d50-8e2a-0b957826aea5"}], "historical_migration": false, "sentAt": "2025-11-15T09:17:48.730239+00:00", "api_key": "phc_J1o2BXYxzXBHJeG2mS5hk62ijkTWk38Z385lO0MhU5w"} to url: http://localhost:8010/batch/ -DEBUG:posthog:data uploaded successfully -DEBUG:posthog:data uploaded successfully -DEBUG:posthog:consumer exited. - - - -CAPTURING EXCEPTION - - - -Context enabled state: None -Self capture_exception_code_variables: True -Computed enabled state: True -Trying to attach code variables to frames -tb_frames found -exception found -variables found -variables: {'CircularRef': "", 'CustomObject': "", 'CustomReprClass': "", 'Decimal': "", 'Fraction': "", 'Posthog': "", 'datetime': "", 'os': "", 'posthog': '', 're': "", 'timedelta': "", 'trigger_error': ''} -code_variables added -variables found -variables: {'my_bytearray': "bytearray(b'mutable bytes')", 'my_bytes': "b'hello bytes'", 'my_circular': '', 'my_complex': '(3+4j)', 'my_custom': '', 'my_datetime': 'datetime.datetime(2024, 1, 15, 10, 30, 45)', 'my_decimal': "Decimal('123.456')", 'my_fraction': 'Fraction(3, 4)', 'my_frozenset': 'frozenset({4, 5, 6})', 'my_function': '', 'my_lambda': '. at 0x1077a07c0>', 'my_memoryview': '', 'my_obj': 'CustomObject(value=42)', 'my_range': 'range(0, 10)', 'my_regex': "re.compile('\\\\d+')", 'my_set': '{1, 2, 3}', 'my_timedelta': 'datetime.timedelta(days=5, seconds=10800)'} -code_variables added From 0731af59fbf803136ddf098a47ceba735c6caa3f Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Sat, 15 Nov 2025 13:10:37 +0100 Subject: [PATCH 4/6] fix: ruff --- posthog/exception_utils.py | 2 +- posthog/test/test_exception_capture.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/posthog/exception_utils.py b/posthog/exception_utils.py index f4bebf73..3c03806b 100644 --- a/posthog/exception_utils.py +++ b/posthog/exception_utils.py @@ -972,7 +972,7 @@ def _serialize_variable_value(value, limiter, max_length=1024): result = repr(value) if len(result) > max_length: result = result[: max_length - 3] + "..." - + result_size = len(result) if not limiter.can_add(result_size): return None diff --git a/posthog/test/test_exception_capture.py b/posthog/test/test_exception_capture.py index 84915b02..650bc37d 100644 --- a/posthog/test/test_exception_capture.py +++ b/posthog/test/test_exception_capture.py @@ -392,7 +392,7 @@ def trigger_error(): assert "ZeroDivisionError" in output assert "code_variables" in output - + assert "re.compile(" in output and "\\\\d+" in output assert "datetime.datetime(2024, 1, 15, 10, 30, 45)" in output assert "datetime.timedelta(days=5, seconds=10800)" in output From 0d0dc862c117b646e2207c9374666692f296b1df Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Sat, 15 Nov 2025 13:11:38 +0100 Subject: [PATCH 5/6] fix: remove comments --- posthog/test/test_exception_capture.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/posthog/test/test_exception_capture.py b/posthog/test/test_exception_capture.py index 650bc37d..cd91f212 100644 --- a/posthog/test/test_exception_capture.py +++ b/posthog/test/test_exception_capture.py @@ -96,7 +96,6 @@ def process_data(): assert b"'my_number': 42" in output assert b"'my_bool': 'True'" in output assert b'"my_dict": "{\\"name\\": \\"test\\", \\"value\\": 123}"' in output - # With repr() fallback, objects without custom __repr__ show full repr including module and memory address assert b"<__main__.UnserializableObject object at" in output assert b"'my_password': '$$_posthog_redacted_based_on_masking_rules_$$'" in output assert b"'__should_be_ignored':" not in output @@ -336,7 +335,6 @@ def process_data(): def test_code_variables_repr_fallback(tmpdir): - """Test that repr() is used for variables that can't be JSON-serialized but can be repr'd""" app = tmpdir.join("app.py") app.write( dedent( From e8ace0916f4631fdd4c343c6d25d4ff6d1bfcb13 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Sat, 15 Nov 2025 13:13:03 +0100 Subject: [PATCH 6/6] feat: bump version and add changelog --- CHANGELOG.md | 4 ++++ posthog/version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fd6d30d..15b51b29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 7.0.1 - 2025-11-15 + +Try to use repr() when formatting code variables + # 7.0.0 - 2025-11-11 NB Python 3.9 is no longer supported diff --git a/posthog/version.py b/posthog/version.py index b2db8f69..639be2bc 100644 --- a/posthog/version.py +++ b/posthog/version.py @@ -1,4 +1,4 @@ -VERSION = "7.0.0" +VERSION = "7.0.1" if __name__ == "__main__": print(VERSION, end="") # noqa: T201