Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 7.8.3 - 2026-02-09

fix: do not pattern match long values in code variables

# 7.8.3 - 2026-02-06

fix: openAI input image sanitization
Expand Down
59 changes: 39 additions & 20 deletions posthog/exception_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,29 @@
DEFAULT_MAX_VALUE_LENGTH = 1024

DEFAULT_CODE_VARIABLES_MASK_PATTERNS = [
r"(?i).*password.*",
r"(?i).*secret.*",
r"(?i).*passwd.*",
r"(?i).*pwd.*",
r"(?i).*api_key.*",
r"(?i).*apikey.*",
r"(?i).*auth.*",
r"(?i).*credentials.*",
r"(?i).*privatekey.*",
r"(?i).*private_key.*",
r"(?i).*token.*",
r"(?i).*aws_access_key_id.*",
r"(?i).*_pass",
r"(?i)sk_.*",
r"(?i).*jwt.*",
r"(?i)password",
r"(?i)secret",
r"(?i)passwd",
r"(?i)pwd",
r"(?i)api_key",
r"(?i)apikey",
r"(?i)auth",
r"(?i)credentials",
r"(?i)privatekey",
r"(?i)private_key",
r"(?i)token",
r"(?i)aws_access_key_id",
r"(?i)_pass",
r"(?i)sk_",
r"(?i)jwt",
]

DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS = [r"^__.*"]

CODE_VARIABLES_REDACTED_VALUE = "$$_posthog_redacted_based_on_masking_rules_$$"
CODE_VARIABLES_TOO_LONG_VALUE = "$$_posthog_value_too_long_$$"

_MAX_VALUE_LENGTH_FOR_PATTERN_MATCH = 5_000

DEFAULT_TOTAL_VARIABLES_SIZE_LIMIT = 20 * 1024

Expand Down Expand Up @@ -945,23 +948,37 @@ def _pattern_matches(name, patterns):
return False


def _mask_sensitive_data(value, compiled_mask):
def _mask_sensitive_data(value, compiled_mask, _seen=None):
if not compiled_mask:
return value

if isinstance(value, (dict, list, tuple)):
if _seen is None:
_seen = set()
obj_id = id(value)
if obj_id in _seen:
return "<circular ref>"
_seen.add(obj_id)

if isinstance(value, dict):
result = {}
for k, v in value.items():
key_str = str(k) if not isinstance(k, str) else k
if _pattern_matches(key_str, compiled_mask):
if len(key_str) > _MAX_VALUE_LENGTH_FOR_PATTERN_MATCH:
result[k] = CODE_VARIABLES_TOO_LONG_VALUE
elif _pattern_matches(key_str, compiled_mask):
result[k] = CODE_VARIABLES_REDACTED_VALUE
else:
result[k] = _mask_sensitive_data(v, compiled_mask)
result[k] = _mask_sensitive_data(v, compiled_mask, _seen)
return result
elif isinstance(value, (list, tuple)):
masked_items = [_mask_sensitive_data(item, compiled_mask) for item in value]
masked_items = [
_mask_sensitive_data(item, compiled_mask, _seen) for item in value
]
return type(value)(masked_items)
elif isinstance(value, str):
if len(value) > _MAX_VALUE_LENGTH_FOR_PATTERN_MATCH:
return CODE_VARIABLES_TOO_LONG_VALUE
if _pattern_matches(value, compiled_mask):
return CODE_VARIABLES_REDACTED_VALUE
return value
Expand All @@ -982,7 +999,9 @@ def _serialize_variable_value(value, limiter, max_length=1024, compiled_mask=Non
limiter.add(result_size)
return value
elif isinstance(value, str):
if compiled_mask and _pattern_matches(value, compiled_mask):
if len(value) > _MAX_VALUE_LENGTH_FOR_PATTERN_MATCH:
result = CODE_VARIABLES_TOO_LONG_VALUE
elif compiled_mask and _pattern_matches(value, compiled_mask):
result = CODE_VARIABLES_REDACTED_VALUE
else:
result = value
Expand Down
140 changes: 140 additions & 0 deletions posthog/test/test_exception_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,3 +450,143 @@ def trigger_error():
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 posthog import Posthog

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():
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 "$$_posthog_value_too_long_$$" in output

assert "'long_blob': '$$_posthog_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 posthog import Posthog

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_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 "$$_posthog_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 posthog.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"] == "$$_posthog_redacted_based_on_masking_rules_$$"


def test_mask_sensitive_data_circular_ref():
from posthog.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>"
2 changes: 1 addition & 1 deletion posthog/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION = "7.8.3"
VERSION = "7.8.4"

if __name__ == "__main__":
print(VERSION, end="") # noqa: T201
Loading