posthog 6.7.2__py3-none-any.whl → 6.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- posthog/__init__.py +30 -2
- posthog/ai/anthropic/anthropic.py +4 -5
- posthog/ai/anthropic/anthropic_async.py +33 -70
- posthog/ai/anthropic/anthropic_converter.py +73 -23
- posthog/ai/gemini/gemini.py +11 -10
- posthog/ai/gemini/gemini_converter.py +177 -29
- posthog/ai/langchain/callbacks.py +18 -3
- posthog/ai/openai/openai.py +8 -8
- posthog/ai/openai/openai_async.py +36 -15
- posthog/ai/openai/openai_converter.py +192 -42
- posthog/ai/types.py +2 -19
- posthog/ai/utils.py +124 -118
- posthog/client.py +96 -4
- posthog/contexts.py +81 -0
- posthog/exception_utils.py +192 -0
- posthog/feature_flags.py +26 -10
- posthog/integrations/django.py +157 -19
- posthog/test/test_client.py +43 -0
- posthog/test/test_exception_capture.py +300 -0
- posthog/test/test_feature_flags.py +146 -35
- posthog/test/test_module.py +0 -8
- posthog/version.py +1 -1
- {posthog-6.7.2.dist-info → posthog-6.9.0.dist-info}/METADATA +1 -1
- {posthog-6.7.2.dist-info → posthog-6.9.0.dist-info}/RECORD +27 -27
- {posthog-6.7.2.dist-info → posthog-6.9.0.dist-info}/WHEEL +0 -0
- {posthog-6.7.2.dist-info → posthog-6.9.0.dist-info}/licenses/LICENSE +0 -0
- {posthog-6.7.2.dist-info → posthog-6.9.0.dist-info}/top_level.txt +0 -0
posthog/ai/utils.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import time
|
|
2
2
|
import uuid
|
|
3
|
-
from typing import Any, Callable, Dict, Optional
|
|
4
|
-
|
|
3
|
+
from typing import Any, Callable, Dict, List, Optional, cast
|
|
5
4
|
|
|
6
5
|
from posthog.client import Client as PostHogClient
|
|
7
|
-
from posthog.ai.types import StreamingEventData,
|
|
6
|
+
from posthog.ai.types import FormattedMessage, StreamingEventData, TokenUsage
|
|
8
7
|
from posthog.ai.sanitization import (
|
|
9
8
|
sanitize_openai,
|
|
10
9
|
sanitize_anthropic,
|
|
@@ -14,7 +13,7 @@ from posthog.ai.sanitization import (
|
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def merge_usage_stats(
|
|
17
|
-
target:
|
|
16
|
+
target: TokenUsage, source: TokenUsage, mode: str = "incremental"
|
|
18
17
|
) -> None:
|
|
19
18
|
"""
|
|
20
19
|
Merge streaming usage statistics into target dict, handling None values.
|
|
@@ -25,19 +24,58 @@ def merge_usage_stats(
|
|
|
25
24
|
|
|
26
25
|
Args:
|
|
27
26
|
target: Dictionary to update with usage stats
|
|
28
|
-
source:
|
|
27
|
+
source: TokenUsage that may contain None values
|
|
29
28
|
mode: Either "incremental" or "cumulative"
|
|
30
29
|
"""
|
|
31
30
|
if mode == "incremental":
|
|
32
31
|
# Add new values to existing totals
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
source_input = source.get("input_tokens")
|
|
33
|
+
if source_input is not None:
|
|
34
|
+
current = target.get("input_tokens") or 0
|
|
35
|
+
target["input_tokens"] = current + source_input
|
|
36
|
+
|
|
37
|
+
source_output = source.get("output_tokens")
|
|
38
|
+
if source_output is not None:
|
|
39
|
+
current = target.get("output_tokens") or 0
|
|
40
|
+
target["output_tokens"] = current + source_output
|
|
41
|
+
|
|
42
|
+
source_cache_read = source.get("cache_read_input_tokens")
|
|
43
|
+
if source_cache_read is not None:
|
|
44
|
+
current = target.get("cache_read_input_tokens") or 0
|
|
45
|
+
target["cache_read_input_tokens"] = current + source_cache_read
|
|
46
|
+
|
|
47
|
+
source_cache_creation = source.get("cache_creation_input_tokens")
|
|
48
|
+
if source_cache_creation is not None:
|
|
49
|
+
current = target.get("cache_creation_input_tokens") or 0
|
|
50
|
+
target["cache_creation_input_tokens"] = current + source_cache_creation
|
|
51
|
+
|
|
52
|
+
source_reasoning = source.get("reasoning_tokens")
|
|
53
|
+
if source_reasoning is not None:
|
|
54
|
+
current = target.get("reasoning_tokens") or 0
|
|
55
|
+
target["reasoning_tokens"] = current + source_reasoning
|
|
56
|
+
|
|
57
|
+
source_web_search = source.get("web_search_count")
|
|
58
|
+
if source_web_search is not None:
|
|
59
|
+
current = target.get("web_search_count") or 0
|
|
60
|
+
target["web_search_count"] = max(current, source_web_search)
|
|
61
|
+
|
|
36
62
|
elif mode == "cumulative":
|
|
37
63
|
# Replace with latest values (already cumulative)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
64
|
+
if source.get("input_tokens") is not None:
|
|
65
|
+
target["input_tokens"] = source["input_tokens"]
|
|
66
|
+
if source.get("output_tokens") is not None:
|
|
67
|
+
target["output_tokens"] = source["output_tokens"]
|
|
68
|
+
if source.get("cache_read_input_tokens") is not None:
|
|
69
|
+
target["cache_read_input_tokens"] = source["cache_read_input_tokens"]
|
|
70
|
+
if source.get("cache_creation_input_tokens") is not None:
|
|
71
|
+
target["cache_creation_input_tokens"] = source[
|
|
72
|
+
"cache_creation_input_tokens"
|
|
73
|
+
]
|
|
74
|
+
if source.get("reasoning_tokens") is not None:
|
|
75
|
+
target["reasoning_tokens"] = source["reasoning_tokens"]
|
|
76
|
+
if source.get("web_search_count") is not None:
|
|
77
|
+
target["web_search_count"] = source["web_search_count"]
|
|
78
|
+
|
|
41
79
|
else:
|
|
42
80
|
raise ValueError(f"Invalid mode: {mode}. Must be 'incremental' or 'cumulative'")
|
|
43
81
|
|
|
@@ -64,74 +102,31 @@ def get_model_params(kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
64
102
|
return model_params
|
|
65
103
|
|
|
66
104
|
|
|
67
|
-
def get_usage(response, provider: str) ->
|
|
105
|
+
def get_usage(response, provider: str) -> TokenUsage:
|
|
106
|
+
"""
|
|
107
|
+
Extract usage statistics from response based on provider.
|
|
108
|
+
Delegates to provider-specific converter functions.
|
|
109
|
+
"""
|
|
68
110
|
if provider == "anthropic":
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
111
|
+
from posthog.ai.anthropic.anthropic_converter import (
|
|
112
|
+
extract_anthropic_usage_from_response,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return extract_anthropic_usage_from_response(response)
|
|
75
116
|
elif provider == "openai":
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
reasoning_tokens = 0
|
|
80
|
-
|
|
81
|
-
# responses api
|
|
82
|
-
if hasattr(response.usage, "input_tokens"):
|
|
83
|
-
input_tokens = response.usage.input_tokens
|
|
84
|
-
if hasattr(response.usage, "output_tokens"):
|
|
85
|
-
output_tokens = response.usage.output_tokens
|
|
86
|
-
if hasattr(response.usage, "input_tokens_details") and hasattr(
|
|
87
|
-
response.usage.input_tokens_details, "cached_tokens"
|
|
88
|
-
):
|
|
89
|
-
cached_tokens = response.usage.input_tokens_details.cached_tokens
|
|
90
|
-
if hasattr(response.usage, "output_tokens_details") and hasattr(
|
|
91
|
-
response.usage.output_tokens_details, "reasoning_tokens"
|
|
92
|
-
):
|
|
93
|
-
reasoning_tokens = response.usage.output_tokens_details.reasoning_tokens
|
|
94
|
-
|
|
95
|
-
# chat completions
|
|
96
|
-
if hasattr(response.usage, "prompt_tokens"):
|
|
97
|
-
input_tokens = response.usage.prompt_tokens
|
|
98
|
-
if hasattr(response.usage, "completion_tokens"):
|
|
99
|
-
output_tokens = response.usage.completion_tokens
|
|
100
|
-
if hasattr(response.usage, "prompt_tokens_details") and hasattr(
|
|
101
|
-
response.usage.prompt_tokens_details, "cached_tokens"
|
|
102
|
-
):
|
|
103
|
-
cached_tokens = response.usage.prompt_tokens_details.cached_tokens
|
|
117
|
+
from posthog.ai.openai.openai_converter import (
|
|
118
|
+
extract_openai_usage_from_response,
|
|
119
|
+
)
|
|
104
120
|
|
|
105
|
-
return
|
|
106
|
-
"input_tokens": input_tokens,
|
|
107
|
-
"output_tokens": output_tokens,
|
|
108
|
-
"cache_read_input_tokens": cached_tokens,
|
|
109
|
-
"reasoning_tokens": reasoning_tokens,
|
|
110
|
-
}
|
|
121
|
+
return extract_openai_usage_from_response(response)
|
|
111
122
|
elif provider == "gemini":
|
|
112
|
-
|
|
113
|
-
|
|
123
|
+
from posthog.ai.gemini.gemini_converter import (
|
|
124
|
+
extract_gemini_usage_from_response,
|
|
125
|
+
)
|
|
114
126
|
|
|
115
|
-
|
|
116
|
-
input_tokens = getattr(response.usage_metadata, "prompt_token_count", 0)
|
|
117
|
-
output_tokens = getattr(
|
|
118
|
-
response.usage_metadata, "candidates_token_count", 0
|
|
119
|
-
)
|
|
127
|
+
return extract_gemini_usage_from_response(response)
|
|
120
128
|
|
|
121
|
-
|
|
122
|
-
"input_tokens": input_tokens,
|
|
123
|
-
"output_tokens": output_tokens,
|
|
124
|
-
"cache_read_input_tokens": 0,
|
|
125
|
-
"cache_creation_input_tokens": 0,
|
|
126
|
-
"reasoning_tokens": 0,
|
|
127
|
-
}
|
|
128
|
-
return {
|
|
129
|
-
"input_tokens": 0,
|
|
130
|
-
"output_tokens": 0,
|
|
131
|
-
"cache_read_input_tokens": 0,
|
|
132
|
-
"cache_creation_input_tokens": 0,
|
|
133
|
-
"reasoning_tokens": 0,
|
|
134
|
-
}
|
|
129
|
+
return TokenUsage(input_tokens=0, output_tokens=0)
|
|
135
130
|
|
|
136
131
|
|
|
137
132
|
def format_response(response, provider: str):
|
|
@@ -169,9 +164,12 @@ def extract_available_tool_calls(provider: str, kwargs: Dict[str, Any]):
|
|
|
169
164
|
from posthog.ai.openai.openai_converter import extract_openai_tools
|
|
170
165
|
|
|
171
166
|
return extract_openai_tools(kwargs)
|
|
167
|
+
return None
|
|
172
168
|
|
|
173
169
|
|
|
174
|
-
def merge_system_prompt(
|
|
170
|
+
def merge_system_prompt(
|
|
171
|
+
kwargs: Dict[str, Any], provider: str
|
|
172
|
+
) -> List[FormattedMessage]:
|
|
175
173
|
"""
|
|
176
174
|
Merge system prompts and format messages for the given provider.
|
|
177
175
|
"""
|
|
@@ -182,14 +180,15 @@ def merge_system_prompt(kwargs: Dict[str, Any], provider: str):
|
|
|
182
180
|
system = kwargs.get("system")
|
|
183
181
|
return format_anthropic_input(messages, system)
|
|
184
182
|
elif provider == "gemini":
|
|
185
|
-
from posthog.ai.gemini.gemini_converter import
|
|
183
|
+
from posthog.ai.gemini.gemini_converter import format_gemini_input_with_system
|
|
186
184
|
|
|
187
185
|
contents = kwargs.get("contents", [])
|
|
188
|
-
|
|
186
|
+
config = kwargs.get("config")
|
|
187
|
+
return format_gemini_input_with_system(contents, config)
|
|
189
188
|
elif provider == "openai":
|
|
190
|
-
# For OpenAI, handle both Chat Completions and Responses API
|
|
191
189
|
from posthog.ai.openai.openai_converter import format_openai_input
|
|
192
190
|
|
|
191
|
+
# For OpenAI, handle both Chat Completions and Responses API
|
|
193
192
|
messages_param = kwargs.get("messages")
|
|
194
193
|
input_param = kwargs.get("input")
|
|
195
194
|
|
|
@@ -200,9 +199,11 @@ def merge_system_prompt(kwargs: Dict[str, Any], provider: str):
|
|
|
200
199
|
if kwargs.get("system") is not None:
|
|
201
200
|
has_system = any(msg.get("role") == "system" for msg in messages)
|
|
202
201
|
if not has_system:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
202
|
+
system_msg = cast(
|
|
203
|
+
FormattedMessage,
|
|
204
|
+
{"role": "system", "content": kwargs.get("system")},
|
|
205
|
+
)
|
|
206
|
+
messages = [system_msg] + messages
|
|
206
207
|
|
|
207
208
|
# For Responses API, add instructions to the system prompt if provided
|
|
208
209
|
if kwargs.get("instructions") is not None:
|
|
@@ -220,9 +221,11 @@ def merge_system_prompt(kwargs: Dict[str, Any], provider: str):
|
|
|
220
221
|
)
|
|
221
222
|
else:
|
|
222
223
|
# Create a new system message with instructions
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
instruction_msg = cast(
|
|
225
|
+
FormattedMessage,
|
|
226
|
+
{"role": "system", "content": kwargs.get("instructions")},
|
|
227
|
+
)
|
|
228
|
+
messages = [instruction_msg] + messages
|
|
226
229
|
|
|
227
230
|
return messages
|
|
228
231
|
|
|
@@ -250,7 +253,7 @@ def call_llm_and_track_usage(
|
|
|
250
253
|
response = None
|
|
251
254
|
error = None
|
|
252
255
|
http_status = 200
|
|
253
|
-
usage:
|
|
256
|
+
usage: TokenUsage = TokenUsage()
|
|
254
257
|
error_params: Dict[str, Any] = {}
|
|
255
258
|
|
|
256
259
|
try:
|
|
@@ -305,27 +308,21 @@ def call_llm_and_track_usage(
|
|
|
305
308
|
if available_tool_calls:
|
|
306
309
|
event_properties["$ai_tools"] = available_tool_calls
|
|
307
310
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
):
|
|
312
|
-
event_properties["$ai_cache_read_input_tokens"] = usage.get(
|
|
313
|
-
"cache_read_input_tokens", 0
|
|
314
|
-
)
|
|
311
|
+
cache_read = usage.get("cache_read_input_tokens")
|
|
312
|
+
if cache_read is not None and cache_read > 0:
|
|
313
|
+
event_properties["$ai_cache_read_input_tokens"] = cache_read
|
|
315
314
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
):
|
|
320
|
-
event_properties["$ai_cache_creation_input_tokens"] = usage.get(
|
|
321
|
-
"cache_creation_input_tokens", 0
|
|
322
|
-
)
|
|
315
|
+
cache_creation = usage.get("cache_creation_input_tokens")
|
|
316
|
+
if cache_creation is not None and cache_creation > 0:
|
|
317
|
+
event_properties["$ai_cache_creation_input_tokens"] = cache_creation
|
|
323
318
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
319
|
+
reasoning = usage.get("reasoning_tokens")
|
|
320
|
+
if reasoning is not None and reasoning > 0:
|
|
321
|
+
event_properties["$ai_reasoning_tokens"] = reasoning
|
|
322
|
+
|
|
323
|
+
web_search_count = usage.get("web_search_count")
|
|
324
|
+
if web_search_count is not None and web_search_count > 0:
|
|
325
|
+
event_properties["$ai_web_search_count"] = web_search_count
|
|
329
326
|
|
|
330
327
|
if posthog_distinct_id is None:
|
|
331
328
|
event_properties["$process_person_profile"] = False
|
|
@@ -367,7 +364,7 @@ async def call_llm_and_track_usage_async(
|
|
|
367
364
|
response = None
|
|
368
365
|
error = None
|
|
369
366
|
http_status = 200
|
|
370
|
-
usage:
|
|
367
|
+
usage: TokenUsage = TokenUsage()
|
|
371
368
|
error_params: Dict[str, Any] = {}
|
|
372
369
|
|
|
373
370
|
try:
|
|
@@ -422,21 +419,21 @@ async def call_llm_and_track_usage_async(
|
|
|
422
419
|
if available_tool_calls:
|
|
423
420
|
event_properties["$ai_tools"] = available_tool_calls
|
|
424
421
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
):
|
|
429
|
-
event_properties["$ai_cache_read_input_tokens"] = usage.get(
|
|
430
|
-
"cache_read_input_tokens", 0
|
|
431
|
-
)
|
|
422
|
+
cache_read = usage.get("cache_read_input_tokens")
|
|
423
|
+
if cache_read is not None and cache_read > 0:
|
|
424
|
+
event_properties["$ai_cache_read_input_tokens"] = cache_read
|
|
432
425
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
426
|
+
cache_creation = usage.get("cache_creation_input_tokens")
|
|
427
|
+
if cache_creation is not None and cache_creation > 0:
|
|
428
|
+
event_properties["$ai_cache_creation_input_tokens"] = cache_creation
|
|
429
|
+
|
|
430
|
+
reasoning = usage.get("reasoning_tokens")
|
|
431
|
+
if reasoning is not None and reasoning > 0:
|
|
432
|
+
event_properties["$ai_reasoning_tokens"] = reasoning
|
|
433
|
+
|
|
434
|
+
web_search_count = usage.get("web_search_count")
|
|
435
|
+
if web_search_count is not None and web_search_count > 0:
|
|
436
|
+
event_properties["$ai_web_search_count"] = web_search_count
|
|
440
437
|
|
|
441
438
|
if posthog_distinct_id is None:
|
|
442
439
|
event_properties["$process_person_profile"] = False
|
|
@@ -559,6 +556,15 @@ def capture_streaming_event(
|
|
|
559
556
|
if value is not None and isinstance(value, int) and value > 0:
|
|
560
557
|
event_properties[f"$ai_{field}"] = value
|
|
561
558
|
|
|
559
|
+
# Add web search count if present (all providers)
|
|
560
|
+
web_search_count = event_data["usage_stats"].get("web_search_count")
|
|
561
|
+
if (
|
|
562
|
+
web_search_count is not None
|
|
563
|
+
and isinstance(web_search_count, int)
|
|
564
|
+
and web_search_count > 0
|
|
565
|
+
):
|
|
566
|
+
event_properties["$ai_web_search_count"] = web_search_count
|
|
567
|
+
|
|
562
568
|
# Handle provider-specific fields
|
|
563
569
|
if (
|
|
564
570
|
event_data["provider"] == "openai"
|
posthog/client.py
CHANGED
|
@@ -19,8 +19,15 @@ from posthog.exception_utils import (
|
|
|
19
19
|
handle_in_app,
|
|
20
20
|
exception_is_already_captured,
|
|
21
21
|
mark_exception_as_captured,
|
|
22
|
+
try_attach_code_variables_to_frames,
|
|
23
|
+
DEFAULT_CODE_VARIABLES_MASK_PATTERNS,
|
|
24
|
+
DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS,
|
|
25
|
+
)
|
|
26
|
+
from posthog.feature_flags import (
|
|
27
|
+
InconclusiveMatchError,
|
|
28
|
+
RequiresServerEvaluation,
|
|
29
|
+
match_feature_flag_properties,
|
|
22
30
|
)
|
|
23
|
-
from posthog.feature_flags import InconclusiveMatchError, match_feature_flag_properties
|
|
24
31
|
from posthog.poller import Poller
|
|
25
32
|
from posthog.request import (
|
|
26
33
|
DEFAULT_HOST,
|
|
@@ -35,6 +42,9 @@ from posthog.contexts import (
|
|
|
35
42
|
_get_current_context,
|
|
36
43
|
get_context_distinct_id,
|
|
37
44
|
get_context_session_id,
|
|
45
|
+
get_capture_exception_code_variables_context,
|
|
46
|
+
get_code_variables_mask_patterns_context,
|
|
47
|
+
get_code_variables_ignore_patterns_context,
|
|
38
48
|
new_context,
|
|
39
49
|
)
|
|
40
50
|
from posthog.types import (
|
|
@@ -56,7 +66,6 @@ from posthog.utils import (
|
|
|
56
66
|
SizeLimitedDict,
|
|
57
67
|
clean,
|
|
58
68
|
guess_timezone,
|
|
59
|
-
remove_trailing_slash,
|
|
60
69
|
system_context,
|
|
61
70
|
)
|
|
62
71
|
from posthog.version import VERSION
|
|
@@ -99,6 +108,34 @@ def add_context_tags(properties):
|
|
|
99
108
|
return properties
|
|
100
109
|
|
|
101
110
|
|
|
111
|
+
def no_throw(default_return=None):
|
|
112
|
+
"""
|
|
113
|
+
Decorator to prevent raising exceptions from public API methods.
|
|
114
|
+
Note that this doesn't prevent errors from propagating via `on_error`.
|
|
115
|
+
Exceptions will still be raised if the debug flag is enabled.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
default_return: Value to return on exception (default: None)
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def decorator(func):
|
|
122
|
+
from functools import wraps
|
|
123
|
+
|
|
124
|
+
@wraps(func)
|
|
125
|
+
def wrapper(self, *args, **kwargs):
|
|
126
|
+
try:
|
|
127
|
+
return func(self, *args, **kwargs)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
if self.debug:
|
|
130
|
+
raise e
|
|
131
|
+
self.log.exception(f"Error in {func.__name__}: {e}")
|
|
132
|
+
return default_return
|
|
133
|
+
|
|
134
|
+
return wrapper
|
|
135
|
+
|
|
136
|
+
return decorator
|
|
137
|
+
|
|
138
|
+
|
|
102
139
|
class Client(object):
|
|
103
140
|
"""
|
|
104
141
|
This is the SDK reference for the PostHog Python SDK.
|
|
@@ -147,6 +184,9 @@ class Client(object):
|
|
|
147
184
|
before_send=None,
|
|
148
185
|
flag_fallback_cache_url=None,
|
|
149
186
|
enable_local_evaluation=True,
|
|
187
|
+
capture_exception_code_variables=False,
|
|
188
|
+
code_variables_mask_patterns=None,
|
|
189
|
+
code_variables_ignore_patterns=None,
|
|
150
190
|
):
|
|
151
191
|
"""
|
|
152
192
|
Initialize a new PostHog client instance.
|
|
@@ -202,6 +242,18 @@ class Client(object):
|
|
|
202
242
|
self.privacy_mode = privacy_mode
|
|
203
243
|
self.enable_local_evaluation = enable_local_evaluation
|
|
204
244
|
|
|
245
|
+
self.capture_exception_code_variables = capture_exception_code_variables
|
|
246
|
+
self.code_variables_mask_patterns = (
|
|
247
|
+
code_variables_mask_patterns
|
|
248
|
+
if code_variables_mask_patterns is not None
|
|
249
|
+
else DEFAULT_CODE_VARIABLES_MASK_PATTERNS
|
|
250
|
+
)
|
|
251
|
+
self.code_variables_ignore_patterns = (
|
|
252
|
+
code_variables_ignore_patterns
|
|
253
|
+
if code_variables_ignore_patterns is not None
|
|
254
|
+
else DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS
|
|
255
|
+
)
|
|
256
|
+
|
|
205
257
|
if project_root is None:
|
|
206
258
|
try:
|
|
207
259
|
project_root = os.getcwd()
|
|
@@ -481,6 +533,7 @@ class Client(object):
|
|
|
481
533
|
|
|
482
534
|
return normalize_flags_response(resp_data)
|
|
483
535
|
|
|
536
|
+
@no_throw()
|
|
484
537
|
def capture(
|
|
485
538
|
self, event: str, **kwargs: Unpack[OptionalCaptureArgs]
|
|
486
539
|
) -> Optional[str]:
|
|
@@ -657,6 +710,7 @@ class Client(object):
|
|
|
657
710
|
f"Expected bool or dict."
|
|
658
711
|
)
|
|
659
712
|
|
|
713
|
+
@no_throw()
|
|
660
714
|
def set(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
|
|
661
715
|
"""
|
|
662
716
|
Set properties on a person profile.
|
|
@@ -690,6 +744,8 @@ class Client(object):
|
|
|
690
744
|
|
|
691
745
|
Category:
|
|
692
746
|
Identification
|
|
747
|
+
|
|
748
|
+
Note: This method will not raise exceptions. Errors are logged.
|
|
693
749
|
"""
|
|
694
750
|
distinct_id = kwargs.get("distinct_id", None)
|
|
695
751
|
properties = kwargs.get("properties", None)
|
|
@@ -716,6 +772,7 @@ class Client(object):
|
|
|
716
772
|
|
|
717
773
|
return self._enqueue(msg, disable_geoip)
|
|
718
774
|
|
|
775
|
+
@no_throw()
|
|
719
776
|
def set_once(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
|
|
720
777
|
"""
|
|
721
778
|
Set properties on a person profile only if they haven't been set before.
|
|
@@ -734,6 +791,8 @@ class Client(object):
|
|
|
734
791
|
|
|
735
792
|
Category:
|
|
736
793
|
Identification
|
|
794
|
+
|
|
795
|
+
Note: This method will not raise exceptions. Errors are logged.
|
|
737
796
|
"""
|
|
738
797
|
distinct_id = kwargs.get("distinct_id", None)
|
|
739
798
|
properties = kwargs.get("properties", None)
|
|
@@ -759,6 +818,7 @@ class Client(object):
|
|
|
759
818
|
|
|
760
819
|
return self._enqueue(msg, disable_geoip)
|
|
761
820
|
|
|
821
|
+
@no_throw()
|
|
762
822
|
def group_identify(
|
|
763
823
|
self,
|
|
764
824
|
group_type: str,
|
|
@@ -791,6 +851,8 @@ class Client(object):
|
|
|
791
851
|
|
|
792
852
|
Category:
|
|
793
853
|
Identification
|
|
854
|
+
|
|
855
|
+
Note: This method will not raise exceptions. Errors are logged.
|
|
794
856
|
"""
|
|
795
857
|
properties = properties or {}
|
|
796
858
|
|
|
@@ -815,6 +877,7 @@ class Client(object):
|
|
|
815
877
|
|
|
816
878
|
return self._enqueue(msg, disable_geoip)
|
|
817
879
|
|
|
880
|
+
@no_throw()
|
|
818
881
|
def alias(
|
|
819
882
|
self,
|
|
820
883
|
previous_id: str,
|
|
@@ -840,6 +903,8 @@ class Client(object):
|
|
|
840
903
|
|
|
841
904
|
Category:
|
|
842
905
|
Identification
|
|
906
|
+
|
|
907
|
+
Note: This method will not raise exceptions. Errors are logged.
|
|
843
908
|
"""
|
|
844
909
|
(distinct_id, personless) = get_identity_state(distinct_id)
|
|
845
910
|
|
|
@@ -932,10 +997,37 @@ class Client(object):
|
|
|
932
997
|
"value"
|
|
933
998
|
),
|
|
934
999
|
"$exception_list": all_exceptions_with_trace_and_in_app,
|
|
935
|
-
"$exception_personURL": f"{remove_trailing_slash(self.raw_host)}/project/{self.api_key}/person/{distinct_id}",
|
|
936
1000
|
**properties,
|
|
937
1001
|
}
|
|
938
1002
|
|
|
1003
|
+
context_enabled = get_capture_exception_code_variables_context()
|
|
1004
|
+
context_mask = get_code_variables_mask_patterns_context()
|
|
1005
|
+
context_ignore = get_code_variables_ignore_patterns_context()
|
|
1006
|
+
|
|
1007
|
+
enabled = (
|
|
1008
|
+
context_enabled
|
|
1009
|
+
if context_enabled is not None
|
|
1010
|
+
else self.capture_exception_code_variables
|
|
1011
|
+
)
|
|
1012
|
+
mask_patterns = (
|
|
1013
|
+
context_mask
|
|
1014
|
+
if context_mask is not None
|
|
1015
|
+
else self.code_variables_mask_patterns
|
|
1016
|
+
)
|
|
1017
|
+
ignore_patterns = (
|
|
1018
|
+
context_ignore
|
|
1019
|
+
if context_ignore is not None
|
|
1020
|
+
else self.code_variables_ignore_patterns
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
if enabled:
|
|
1024
|
+
try_attach_code_variables_to_frames(
|
|
1025
|
+
all_exceptions_with_trace_and_in_app,
|
|
1026
|
+
exc_info,
|
|
1027
|
+
mask_patterns=mask_patterns,
|
|
1028
|
+
ignore_patterns=ignore_patterns,
|
|
1029
|
+
)
|
|
1030
|
+
|
|
939
1031
|
if self.log_captured_exceptions:
|
|
940
1032
|
self.log.exception(exception, extra=kwargs)
|
|
941
1033
|
|
|
@@ -1543,7 +1635,7 @@ class Client(object):
|
|
|
1543
1635
|
self.log.debug(
|
|
1544
1636
|
f"Successfully computed flag locally: {key} -> {response}"
|
|
1545
1637
|
)
|
|
1546
|
-
except InconclusiveMatchError as e:
|
|
1638
|
+
except (RequiresServerEvaluation, InconclusiveMatchError) as e:
|
|
1547
1639
|
self.log.debug(f"Failed to compute flag {key} locally: {e}")
|
|
1548
1640
|
except Exception as e:
|
|
1549
1641
|
self.log.exception(
|
posthog/contexts.py
CHANGED
|
@@ -22,6 +22,9 @@ class ContextScope:
|
|
|
22
22
|
self.session_id: Optional[str] = None
|
|
23
23
|
self.distinct_id: Optional[str] = None
|
|
24
24
|
self.tags: Dict[str, Any] = {}
|
|
25
|
+
self.capture_exception_code_variables: Optional[bool] = None
|
|
26
|
+
self.code_variables_mask_patterns: Optional[list] = None
|
|
27
|
+
self.code_variables_ignore_patterns: Optional[list] = None
|
|
25
28
|
|
|
26
29
|
def set_session_id(self, session_id: str):
|
|
27
30
|
self.session_id = session_id
|
|
@@ -32,6 +35,15 @@ class ContextScope:
|
|
|
32
35
|
def add_tag(self, key: str, value: Any):
|
|
33
36
|
self.tags[key] = value
|
|
34
37
|
|
|
38
|
+
def set_capture_exception_code_variables(self, enabled: bool):
|
|
39
|
+
self.capture_exception_code_variables = enabled
|
|
40
|
+
|
|
41
|
+
def set_code_variables_mask_patterns(self, mask_patterns: list):
|
|
42
|
+
self.code_variables_mask_patterns = mask_patterns
|
|
43
|
+
|
|
44
|
+
def set_code_variables_ignore_patterns(self, ignore_patterns: list):
|
|
45
|
+
self.code_variables_ignore_patterns = ignore_patterns
|
|
46
|
+
|
|
35
47
|
def get_parent(self):
|
|
36
48
|
return self.parent
|
|
37
49
|
|
|
@@ -59,6 +71,27 @@ class ContextScope:
|
|
|
59
71
|
tags.update(new_tags)
|
|
60
72
|
return tags
|
|
61
73
|
|
|
74
|
+
def get_capture_exception_code_variables(self) -> Optional[bool]:
|
|
75
|
+
if self.capture_exception_code_variables is not None:
|
|
76
|
+
return self.capture_exception_code_variables
|
|
77
|
+
if self.parent is not None and not self.fresh:
|
|
78
|
+
return self.parent.get_capture_exception_code_variables()
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
def get_code_variables_mask_patterns(self) -> Optional[list]:
|
|
82
|
+
if self.code_variables_mask_patterns is not None:
|
|
83
|
+
return self.code_variables_mask_patterns
|
|
84
|
+
if self.parent is not None and not self.fresh:
|
|
85
|
+
return self.parent.get_code_variables_mask_patterns()
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def get_code_variables_ignore_patterns(self) -> Optional[list]:
|
|
89
|
+
if self.code_variables_ignore_patterns is not None:
|
|
90
|
+
return self.code_variables_ignore_patterns
|
|
91
|
+
if self.parent is not None and not self.fresh:
|
|
92
|
+
return self.parent.get_code_variables_ignore_patterns()
|
|
93
|
+
return None
|
|
94
|
+
|
|
62
95
|
|
|
63
96
|
_context_stack: contextvars.ContextVar[Optional[ContextScope]] = contextvars.ContextVar(
|
|
64
97
|
"posthog_context_stack", default=None
|
|
@@ -243,6 +276,54 @@ def get_context_distinct_id() -> Optional[str]:
|
|
|
243
276
|
return None
|
|
244
277
|
|
|
245
278
|
|
|
279
|
+
def set_capture_exception_code_variables_context(enabled: bool) -> None:
|
|
280
|
+
"""
|
|
281
|
+
Set whether code variables are captured for the current context.
|
|
282
|
+
"""
|
|
283
|
+
current_context = _get_current_context()
|
|
284
|
+
if current_context:
|
|
285
|
+
current_context.set_capture_exception_code_variables(enabled)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def set_code_variables_mask_patterns_context(mask_patterns: list) -> None:
|
|
289
|
+
"""
|
|
290
|
+
Variable names matching these patterns will be masked with *** when capturing code variables.
|
|
291
|
+
"""
|
|
292
|
+
current_context = _get_current_context()
|
|
293
|
+
if current_context:
|
|
294
|
+
current_context.set_code_variables_mask_patterns(mask_patterns)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def set_code_variables_ignore_patterns_context(ignore_patterns: list) -> None:
|
|
298
|
+
"""
|
|
299
|
+
Variable names matching these patterns will be ignored completely when capturing code variables.
|
|
300
|
+
"""
|
|
301
|
+
current_context = _get_current_context()
|
|
302
|
+
if current_context:
|
|
303
|
+
current_context.set_code_variables_ignore_patterns(ignore_patterns)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def get_capture_exception_code_variables_context() -> Optional[bool]:
|
|
307
|
+
current_context = _get_current_context()
|
|
308
|
+
if current_context:
|
|
309
|
+
return current_context.get_capture_exception_code_variables()
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def get_code_variables_mask_patterns_context() -> Optional[list]:
|
|
314
|
+
current_context = _get_current_context()
|
|
315
|
+
if current_context:
|
|
316
|
+
return current_context.get_code_variables_mask_patterns()
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def get_code_variables_ignore_patterns_context() -> Optional[list]:
|
|
321
|
+
current_context = _get_current_context()
|
|
322
|
+
if current_context:
|
|
323
|
+
return current_context.get_code_variables_ignore_patterns()
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
|
|
246
327
|
F = TypeVar("F", bound=Callable[..., Any])
|
|
247
328
|
|
|
248
329
|
|