Files
zeekay 3ca982343e rebrand: remove all compat aliases, zero posthog refs
- Remove Posthog = Insights alias from __init__.py
- Remove PosthogContextMiddleware alias from django.py
- Remove PostHogTracingProcessor alias from processor.py
- Change $lib from "posthog-python" to "insights-python"
- Change ingestion URLs from posthog.com to insights.hanzo.ai
- Rename all posthog_* kwargs to insights_* across AI wrappers
- Rename __posthog_exception_captured to __insights_exception_captured
- Rename posthog_context_stack contextvar to insights_context_stack
- Rename posthog🎏 Redis prefix to insights🎏
- Rename $$_posthog_redacted_* sentinels to $$_insights_redacted_*
- Remove POSTHOG_MW_* Django settings fallback, X-POSTHOG-* headers
- Rename Prompts(posthog=) param to Prompts(client=)
- Update APP_ENDPOINT to us.insights.hanzo.ai
- Update all tests, examples, docs, mypy config
2026-03-13 20:29:13 -07:00

690 lines
20 KiB
Python

import subprocess
import sys
from textwrap import dedent
import pytest
def test_excepthook(tmpdir):
app = tmpdir.join("app.py")
app.write(
dedent(
"""
from hanzo_insights import Insights
client = Insights('phc_x', host='https://eu.i.insights.hanzo.ai', enable_exception_autocapture=True, debug=True, on_error=lambda e, batch: print('error handling batch: ', e, batch))
# frame_value = "LOL"
1/0
"""
)
)
with pytest.raises(subprocess.CalledProcessError) as excinfo:
subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
output = excinfo.value.output
assert b"ZeroDivisionError" in output
assert b"LOL" in output
assert b"DEBUG:hanzo_insights:data uploaded successfully" in output
assert (
b'"$exception_list": [{"mechanism": {"type": "generic", "handled": true}, "module": null, "type": "ZeroDivisionError", "value": "division by zero", "stacktrace": {"frames": [{"platform": "python", "filename": "app.py", "abs_path"'
in output
)
def test_code_variables_capture(tmpdir):
app = tmpdir.join("app.py")
app.write(
dedent(
"""
import os
from hanzo_insights import Insights
class UnserializableObject:
pass
client = Insights(
'phc_x',
host='https://eu.i.insights.hanzo.ai',
debug=True,
enable_exception_autocapture=True,
capture_exception_code_variables=True,
project_root=os.path.dirname(os.path.abspath(__file__))
)
def trigger_error():
my_string = "hello world"
my_number = 42
my_bool = True
my_dict = {"name": "test", "value": 123}
my_sensitive_dict = {
"safe_key": "safe_value",
"password": "secret123", # key matches pattern -> should be masked
"other_key": "contains_password_here", # value matches pattern -> should be masked
}
my_nested_dict = {
"level1": {
"level2": {
"api_key": "nested_secret", # deeply nested key matches
"data": "contains_token_here", # deeply nested value matches
"safe": "visible",
}
}
}
my_list = ["safe_item", "has_password_inside", "another_safe"]
my_tuple = ("tuple_safe", "secret_in_value", "tuple_also_safe")
my_list_of_dicts = [
{"id": 1, "password": "list_dict_secret"},
{"id": 2, "value": "safe_value"},
]
my_obj = UnserializableObject()
my_password = "secret123" # Should be masked by default (name matches)
my_innocent_var = "contains_password_here" # Should be masked by default (value matches)
__should_be_ignored = "hidden" # Should be ignored by default
1/0 # Trigger exception
def intermediate_function():
request_id = "abc-123"
user_count = 100
is_active = True
trigger_error()
def process_data():
batch_size = 50
retry_count = 3
intermediate_function()
process_data()
"""
)
)
with pytest.raises(subprocess.CalledProcessError) as excinfo:
subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
output = excinfo.value.output
assert b"ZeroDivisionError" in output
assert b"code_variables" in output
# Variables from trigger_error frame
assert b"'my_string': 'hello world'" in output
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'{\\"safe_key\\": \\"safe_value\\", \\"password\\": \\"$$_insights_redacted_based_on_masking_rules_$$\\", \\"other_key\\": \\"$$_insights_redacted_based_on_masking_rules_$$\\"}'
in output
)
assert (
b'{\\"level1\\": {\\"level2\\": {\\"api_key\\": \\"$$_insights_redacted_based_on_masking_rules_$$\\", \\"data\\": \\"$$_insights_redacted_based_on_masking_rules_$$\\", \\"safe\\": \\"visible\\"}}}'
in output
)
assert (
b'[\\"safe_item\\", \\"$$_insights_redacted_based_on_masking_rules_$$\\", \\"another_safe\\"]'
in output
)
assert (
b'[\\"tuple_safe\\", \\"$$_insights_redacted_based_on_masking_rules_$$\\", \\"tuple_also_safe\\"]'
in output
)
assert (
b'[{\\"id\\": 1, \\"password\\": \\"$$_insights_redacted_based_on_masking_rules_$$\\"}, {\\"id\\": 2, \\"value\\": \\"safe_value\\"}]'
in output
)
assert b"<__main__.UnserializableObject object at" in output
assert b"'my_password': '$$_insights_redacted_based_on_masking_rules_$$'" in output
assert (
b"'my_innocent_var': '$$_insights_redacted_based_on_masking_rules_$$'" in output
)
assert b"'__should_be_ignored':" not in output
# Variables from intermediate_function frame
assert b"'request_id': 'abc-123'" in output
assert b"'user_count': 100" in output
assert b"'is_active': 'True'" in output
# Variables from process_data frame
assert b"'batch_size': 50" in output
assert b"'retry_count': 3" in output
def test_code_variables_context_override(tmpdir):
app = tmpdir.join("app.py")
app.write(
dedent(
"""
import os
import hanzo_insights
from hanzo_insights import Insights
insights_client = Insights(
'phc_x',
host='https://eu.i.insights.hanzo.ai',
debug=True,
enable_exception_autocapture=True,
capture_exception_code_variables=False,
project_root=os.path.dirname(os.path.abspath(__file__))
)
def process_data():
bank = "should_be_masked"
__dunder_var = "should_be_visible"
1/0
with hanzo_insights.new_context(client=insights_client):
hanzo_insights.set_capture_exception_code_variables_context(True)
hanzo_insights.set_code_variables_mask_patterns_context([r"(?i).*bank.*"])
hanzo_insights.set_code_variables_ignore_patterns_context([])
process_data()
"""
)
)
with pytest.raises(subprocess.CalledProcessError) as excinfo:
subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
output = excinfo.value.output
assert b"ZeroDivisionError" in output
assert b"code_variables" in output
assert b"'bank': '$$_insights_redacted_based_on_masking_rules_$$'" in output
assert b"'__dunder_var': 'should_be_visible'" in output
def test_code_variables_size_limiter(tmpdir):
app = tmpdir.join("app.py")
app.write(
dedent(
"""
import os
from hanzo_insights import Insights
client = Insights(
'phc_x',
host='https://eu.i.insights.hanzo.ai',
debug=True,
enable_exception_autocapture=True,
capture_exception_code_variables=True,
project_root=os.path.dirname(os.path.abspath(__file__))
)
def trigger_error():
var_a = "a" * 2000
var_b = "b" * 2000
var_c = "c" * 2000
var_d = "d" * 2000
var_e = "e" * 2000
var_f = "f" * 2000
var_g = "g" * 2000
1/0
def intermediate_function():
var_h = "h" * 2000
var_i = "i" * 2000
var_j = "j" * 2000
var_k = "k" * 2000
var_l = "l" * 2000
var_m = "m" * 2000
var_n = "n" * 2000
trigger_error()
def process_data():
var_o = "o" * 2000
var_p = "p" * 2000
var_q = "q" * 2000
var_r = "r" * 2000
var_s = "s" * 2000
var_t = "t" * 2000
var_u = "u" * 2000
intermediate_function()
process_data()
"""
)
)
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
captured_vars = []
for var_name in [
"var_a",
"var_b",
"var_c",
"var_d",
"var_e",
"var_f",
"var_g",
"var_h",
"var_i",
"var_j",
"var_k",
"var_l",
"var_m",
"var_n",
"var_o",
"var_p",
"var_q",
"var_r",
"var_s",
"var_t",
"var_u",
]:
if f"'{var_name}'" in output:
captured_vars.append(var_name)
assert len(captured_vars) > 0
assert len(captured_vars) < 21
def test_code_variables_disabled_capture(tmpdir):
app = tmpdir.join("app.py")
app.write(
dedent(
"""
import os
from hanzo_insights import Insights
client = Insights(
'phc_x',
host='https://eu.i.insights.hanzo.ai',
debug=True,
enable_exception_autocapture=True,
capture_exception_code_variables=False,
project_root=os.path.dirname(os.path.abspath(__file__))
)
def trigger_error():
my_string = "hello world"
my_number = 42
my_bool = True
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':" not in output
assert '"code_variables":' not in output
assert "'my_string'" not in output
assert "'my_number'" not in output
def test_code_variables_enabled_then_disabled_in_context(tmpdir):
app = tmpdir.join("app.py")
app.write(
dedent(
"""
import os
import hanzo_insights
from hanzo_insights import Insights
insights_client = Insights(
'phc_x',
host='https://eu.i.insights.hanzo.ai',
debug=True,
enable_exception_autocapture=True,
capture_exception_code_variables=True,
project_root=os.path.dirname(os.path.abspath(__file__))
)
def process_data():
my_var = "should not be captured"
important_value = 123
1/0
with hanzo_insights.new_context(client=insights_client):
hanzo_insights.set_capture_exception_code_variables_context(False)
process_data()
"""
)
)
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':" not in output
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):
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 hanzo_insights import Insights
class CustomReprClass:
def __repr__(self):
return '<CustomReprClass: custom representation>'
client = Insights(
'phc_x',
host='https://eu.i.insights.hanzo.ai',
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 "<memory at" in output
assert "(3+4j)" in output
assert "range(0, 10)" in output
assert "<CustomReprClass: custom representation>" in output
assert "<lambda>" in output
assert "<function trigger_error at" in output
def test_code_variables_too_long_string_value_replaced(tmpdir):
app = tmpdir.join("app.py")
app.write(
dedent(
"""
import os
from hanzo_insights import Insights
client = Insights(
'phc_x',
host='https://eu.i.insights.hanzo.ai',
debug=True,
enable_exception_autocapture=True,
capture_exception_code_variables=True,
project_root=os.path.dirname(os.path.abspath(__file__))
)
def trigger_error():
short_value = "I am short"
long_value = "x" * 20000
long_blob = "password_" + "a" * 20000
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 "'short_value': 'I am short'" in output
assert "$$_insights_value_too_long_$$" in output
assert "'long_blob': '$$_insights_value_too_long_$$'" in output
def test_code_variables_too_long_string_in_nested_dict(tmpdir):
app = tmpdir.join("app.py")
app.write(
dedent(
"""
import os
from hanzo_insights import Insights
client = Insights(
'phc_x',
host='https://eu.i.insights.hanzo.ai',
debug=True,
enable_exception_autocapture=True,
capture_exception_code_variables=True,
project_root=os.path.dirname(os.path.abspath(__file__))
)
def trigger_error():
my_data = {
"short_key": "short_val",
"long_key": "y" * 20000,
"nested": {
"deep_long": "z" * 20000,
"deep_short": "ok",
},
}
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 "short_val" in output
assert "ok" in output
assert "$$_insights_value_too_long_$$" in output
assert "y" * 1000 not in output
assert "z" * 1000 not in output
def test_mask_sensitive_data_too_long_dict_key():
from hanzo_insights.exception_utils import (
CODE_VARIABLES_TOO_LONG_VALUE,
_compile_patterns,
_mask_sensitive_data,
)
compiled_mask = _compile_patterns([r"(?i)password"])
result = _mask_sensitive_data(
{
"short": "visible",
"k" * 20000: "hidden_val",
"password": "secret",
},
compiled_mask,
)
assert result["short"] == "visible"
# This then gets shortened by the JSON truncation at 1024 chars anyways so no worries
assert result["k" * 20000] == CODE_VARIABLES_TOO_LONG_VALUE
assert result["password"] == "$$_insights_redacted_based_on_masking_rules_$$"
def test_mask_sensitive_data_circular_ref():
from hanzo_insights.exception_utils import _compile_patterns, _mask_sensitive_data
compiled_mask = _compile_patterns([r"(?i)password"])
# Circular dict
circular_dict = {"key": "value"}
circular_dict["self"] = circular_dict
result = _mask_sensitive_data(circular_dict, compiled_mask)
assert result["key"] == "value"
assert result["self"] == "<circular ref>"
# Circular list
circular_list = ["item"]
circular_list.append(circular_list)
result = _mask_sensitive_data(circular_list, compiled_mask)
assert result[0] == "item"
assert result[1] == "<circular ref>"
def test_compile_patterns_fast_path_and_regex_fallback():
from hanzo_insights.exception_utils import _compile_patterns, _pattern_matches
# Simple case-insensitive patterns should become substrings
simple_only = _compile_patterns([r"(?i)password", r"(?i)token", r"(?i)jwt"])
substrings, regexes = simple_only
assert substrings == ["password", "token", "jwt"]
assert regexes == []
assert _pattern_matches("my_password_var", simple_only) is True
assert _pattern_matches("MY_TOKEN", simple_only) is True
assert _pattern_matches("safe_variable", simple_only) is False
# Complex regex patterns should stay as compiled regexes
complex_only = _compile_patterns([r"^__.*", r"\d{3,}", r"^sk_live_"])
substrings, regexes = complex_only
assert substrings == []
assert len(regexes) == 3
assert _pattern_matches("__dunder", complex_only) is True
assert _pattern_matches("has_999_numbers", complex_only) is True
assert _pattern_matches("sk_live_abc123", complex_only) is True
assert _pattern_matches("normal_var", complex_only) is False
# Mixed: simple substrings + complex regexes together
mixed = _compile_patterns(
[
r"(?i)secret", # simple
r"(?i)api_key", # simple
r"^__.*", # regex
r"\btoken_\w+", # regex
]
)
substrings, regexes = mixed
assert substrings == ["secret", "api_key"]
assert len(regexes) == 2
# Substring matches
assert _pattern_matches("my_secret", mixed) is True
assert _pattern_matches("API_KEY_VALUE", mixed) is True
# Regex matches
assert _pattern_matches("__private", mixed) is True
assert _pattern_matches("token_abc", mixed) is True
# No match
assert _pattern_matches("safe_var", mixed) is False
def test_mask_sensitive_data_large_dict_replaced():
from hanzo_insights.exception_utils import (
CODE_VARIABLES_TOO_LONG_VALUE,
_compile_patterns,
_mask_sensitive_data,
)
compiled_mask = _compile_patterns([r"(?i)password"])
large_dict = {f"key_{i}": f"value_{i}" for i in range(300)}
result = _mask_sensitive_data(large_dict, compiled_mask)
assert result == CODE_VARIABLES_TOO_LONG_VALUE
def test_mask_sensitive_data_large_list_replaced():
from hanzo_insights.exception_utils import (
CODE_VARIABLES_TOO_LONG_VALUE,
_compile_patterns,
_mask_sensitive_data,
)
compiled_mask = _compile_patterns([r"(?i)password"])
large_list = [f"item_{i}" for i in range(300)]
result = _mask_sensitive_data(large_list, compiled_mask)
assert result == CODE_VARIABLES_TOO_LONG_VALUE
def test_mask_sensitive_data_large_tuple_replaced():
from hanzo_insights.exception_utils import (
CODE_VARIABLES_TOO_LONG_VALUE,
_compile_patterns,
_mask_sensitive_data,
)
compiled_mask = _compile_patterns([r"(?i)password"])
large_tuple = tuple(f"item_{i}" for i in range(300))
result = _mask_sensitive_data(large_tuple, compiled_mask)
assert result == CODE_VARIABLES_TOO_LONG_VALUE