Skip to content

Commit ce38fb2

Browse files
feat: mask values (#382)
* feat: mask values * feat: wip * fix: ruff * feat: wip * feat: wip * fix: ruff * fix: ruff * feat: wip * feat: wip * feat: version bump
1 parent 103a7ad commit ce38fb2

File tree

4 files changed

+99
-7
lines changed

4 files changed

+99
-7
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
# 7.3.0 - 2025-12-05
2+
3+
feat: improve code variables capture masking
4+
15
# 7.2.0 - 2025-12-01
26

37
feat: add $feature_flag_evaluated_at properties to $feature_flag_called events
48

5-
69
# 7.1.0 - 2025-11-26
710

811
Add support for the async version of Gemini.

posthog/exception_utils.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@
5454
r"(?i).*privatekey.*",
5555
r"(?i).*private_key.*",
5656
r"(?i).*token.*",
57+
r"(?i).*aws_access_key_id.*",
58+
r"(?i).*_pass",
59+
r"(?i)sk_.*",
60+
r"(?i).*jwt.*",
5761
]
5862

5963
DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS = [r"^__.*"]
@@ -941,7 +945,31 @@ def _pattern_matches(name, patterns):
941945
return False
942946

943947

944-
def _serialize_variable_value(value, limiter, max_length=1024):
948+
def _mask_sensitive_data(value, compiled_mask):
949+
if not compiled_mask:
950+
return value
951+
952+
if isinstance(value, dict):
953+
result = {}
954+
for k, v in value.items():
955+
key_str = str(k) if not isinstance(k, str) else k
956+
if _pattern_matches(key_str, compiled_mask):
957+
result[k] = CODE_VARIABLES_REDACTED_VALUE
958+
else:
959+
result[k] = _mask_sensitive_data(v, compiled_mask)
960+
return result
961+
elif isinstance(value, (list, tuple)):
962+
masked_items = [_mask_sensitive_data(item, compiled_mask) for item in value]
963+
return type(value)(masked_items)
964+
elif isinstance(value, str):
965+
if _pattern_matches(value, compiled_mask):
966+
return CODE_VARIABLES_REDACTED_VALUE
967+
return value
968+
else:
969+
return value
970+
971+
972+
def _serialize_variable_value(value, limiter, max_length=1024, compiled_mask=None):
945973
try:
946974
if value is None:
947975
result = "None"
@@ -954,9 +982,13 @@ def _serialize_variable_value(value, limiter, max_length=1024):
954982
limiter.add(result_size)
955983
return value
956984
elif isinstance(value, str):
957-
result = value
985+
if compiled_mask and _pattern_matches(value, compiled_mask):
986+
result = CODE_VARIABLES_REDACTED_VALUE
987+
else:
988+
result = value
958989
else:
959-
result = json.dumps(value)
990+
masked_value = _mask_sensitive_data(value, compiled_mask)
991+
result = json.dumps(masked_value)
960992

961993
if len(result) > max_length:
962994
result = result[: max_length - 3] + "..."
@@ -1043,7 +1075,9 @@ def serialize_code_variables(
10431075
limiter.add(redacted_size)
10441076
result[name] = redacted_value
10451077
else:
1046-
serialized = _serialize_variable_value(value, limiter, max_length)
1078+
serialized = _serialize_variable_value(
1079+
value, limiter, max_length, compiled_mask
1080+
)
10471081
if serialized is None:
10481082
break
10491083
result[name] = serialized
@@ -1053,6 +1087,17 @@ def serialize_code_variables(
10531087

10541088
def try_attach_code_variables_to_frames(
10551089
all_exceptions, exc_info, mask_patterns, ignore_patterns
1090+
):
1091+
try:
1092+
attach_code_variables_to_frames(
1093+
all_exceptions, exc_info, mask_patterns, ignore_patterns
1094+
)
1095+
except Exception:
1096+
pass
1097+
1098+
1099+
def attach_code_variables_to_frames(
1100+
all_exceptions, exc_info, mask_patterns, ignore_patterns
10561101
):
10571102
exc_type, exc_value, traceback = exc_info
10581103

posthog/test/test_exception_capture.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,29 @@ def trigger_error():
5959
my_number = 42
6060
my_bool = True
6161
my_dict = {"name": "test", "value": 123}
62+
my_sensitive_dict = {
63+
"safe_key": "safe_value",
64+
"password": "secret123", # key matches pattern -> should be masked
65+
"other_key": "contains_password_here", # value matches pattern -> should be masked
66+
}
67+
my_nested_dict = {
68+
"level1": {
69+
"level2": {
70+
"api_key": "nested_secret", # deeply nested key matches
71+
"data": "contains_token_here", # deeply nested value matches
72+
"safe": "visible",
73+
}
74+
}
75+
}
76+
my_list = ["safe_item", "has_password_inside", "another_safe"]
77+
my_tuple = ("tuple_safe", "secret_in_value", "tuple_also_safe")
78+
my_list_of_dicts = [
79+
{"id": 1, "password": "list_dict_secret"},
80+
{"id": 2, "value": "safe_value"},
81+
]
6282
my_obj = UnserializableObject()
63-
my_password = "secret123" # Should be masked by default
83+
my_password = "secret123" # Should be masked by default (name matches)
84+
my_innocent_var = "contains_password_here" # Should be masked by default (value matches)
6485
__should_be_ignored = "hidden" # Should be ignored by default
6586
6687
1/0 # Trigger exception
@@ -96,8 +117,31 @@ def process_data():
96117
assert b"'my_number': 42" in output
97118
assert b"'my_bool': 'True'" in output
98119
assert b'"my_dict": "{\\"name\\": \\"test\\", \\"value\\": 123}"' in output
120+
assert (
121+
b'{\\"safe_key\\": \\"safe_value\\", \\"password\\": \\"$$_posthog_redacted_based_on_masking_rules_$$\\", \\"other_key\\": \\"$$_posthog_redacted_based_on_masking_rules_$$\\"}'
122+
in output
123+
)
124+
assert (
125+
b'{\\"level1\\": {\\"level2\\": {\\"api_key\\": \\"$$_posthog_redacted_based_on_masking_rules_$$\\", \\"data\\": \\"$$_posthog_redacted_based_on_masking_rules_$$\\", \\"safe\\": \\"visible\\"}}}'
126+
in output
127+
)
128+
assert (
129+
b'[\\"safe_item\\", \\"$$_posthog_redacted_based_on_masking_rules_$$\\", \\"another_safe\\"]'
130+
in output
131+
)
132+
assert (
133+
b'[\\"tuple_safe\\", \\"$$_posthog_redacted_based_on_masking_rules_$$\\", \\"tuple_also_safe\\"]'
134+
in output
135+
)
136+
assert (
137+
b'[{\\"id\\": 1, \\"password\\": \\"$$_posthog_redacted_based_on_masking_rules_$$\\"}, {\\"id\\": 2, \\"value\\": \\"safe_value\\"}]'
138+
in output
139+
)
99140
assert b"<__main__.UnserializableObject object at" in output
100141
assert b"'my_password': '$$_posthog_redacted_based_on_masking_rules_$$'" in output
142+
assert (
143+
b"'my_innocent_var': '$$_posthog_redacted_based_on_masking_rules_$$'" in output
144+
)
101145
assert b"'__should_be_ignored':" not in output
102146

103147
# Variables from intermediate_function frame

posthog/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
VERSION = "7.2.0"
1+
VERSION = "7.3.0"
22

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

0 commit comments

Comments
 (0)