11import logging
22import sys
33import threading
4- from typing import TYPE_CHECKING
4+ from enum import Enum
5+ from typing import TYPE_CHECKING , List , Optional
56
6- from posthog .exception_utils import exceptions_from_error_tuple
7+ from posthog .exception_utils import exceptions_from_error_tuple , handle_in_app
8+ from posthog .utils import remove_trailing_slash
79
810if TYPE_CHECKING :
911 from posthog .client import Client
1012
1113
14+ class Integrations (str , Enum ):
15+ Django = "django"
16+
17+
18+ DEFAULT_DISTINCT_ID = "python-exceptions"
19+
20+
1221class ExceptionCapture :
1322 # TODO: Add client side rate limiting to prevent spamming the server with exceptions
1423
1524 log = logging .getLogger ("posthog" )
1625
17- def __init__ (self , client : "Client" ):
26+ def __init__ (self , client : "Client" , integrations : Optional [ List [ Integrations ]] = None ):
1827 self .client = client
1928 self .original_excepthook = sys .excepthook
2029 sys .excepthook = self .exception_handler
2130 threading .excepthook = self .thread_exception_handler
31+ self .enabled_integrations = []
32+
33+ for integration in integrations or []:
34+ # TODO: Maybe find a better way of enabling integrations
35+ # This is very annoying currently if we had to add any configuration per integration
36+ if integration == Integrations .Django :
37+ try :
38+ from posthog .exception_integrations .django import DjangoIntegration
39+
40+ enabled_integration = DjangoIntegration (self .exception_receiver )
41+ self .enabled_integrations .append (enabled_integration )
42+ except Exception as e :
43+ self .log .exception (f"Failed to enable Django integration: { e } " )
44+
45+ def close (self ):
46+ sys .excepthook = self .original_excepthook
47+ for integration in self .enabled_integrations :
48+ integration .uninstall ()
2249
2350 def exception_handler (self , exc_type , exc_value , exc_traceback ):
2451 # don't affect default behaviour.
@@ -28,7 +55,14 @@ def exception_handler(self, exc_type, exc_value, exc_traceback):
2855 def thread_exception_handler (self , args ):
2956 self .capture_exception (args .exc_type , args .exc_value , args .exc_traceback )
3057
31- def capture_exception (self , exc_type , exc_value , exc_traceback ):
58+ def exception_receiver (self , exc_info , extra_properties ):
59+ if "distinct_id" in extra_properties :
60+ metadata = {"distinct_id" : extra_properties ["distinct_id" ]}
61+ else :
62+ metadata = None
63+ self .capture_exception (exc_info [0 ], exc_info [1 ], exc_info [2 ], metadata )
64+
65+ def capture_exception (self , exc_type , exc_value , exc_traceback , metadata = None ):
3266 try :
3367 # if hasattr(sys, "ps1"):
3468 # # Disable the excepthook for interactive Python shells
@@ -37,17 +71,30 @@ def capture_exception(self, exc_type, exc_value, exc_traceback):
3771 # Format stack trace like sentry
3872 all_exceptions_with_trace = exceptions_from_error_tuple ((exc_type , exc_value , exc_traceback ))
3973
74+ # Add in-app property to frames in the exceptions
75+ event = handle_in_app (
76+ {
77+ "exception" : {
78+ "values" : all_exceptions_with_trace ,
79+ },
80+ }
81+ )
82+ all_exceptions_with_trace_and_in_app = event ["exception" ]["values" ]
83+
84+ distinct_id = metadata .get ("distinct_id" ) if metadata else DEFAULT_DISTINCT_ID
85+ # Make sure we have a distinct_id if its empty in metadata
86+ distinct_id = distinct_id or DEFAULT_DISTINCT_ID
87+
4088 properties = {
41- "$exception_type" : all_exceptions_with_trace [0 ].get ("type" ),
42- "$exception_message" : all_exceptions_with_trace [0 ].get ("value" ),
43- "$exception_list" : all_exceptions_with_trace ,
44- # TODO: Can we somehow get distinct_id from context here? Stateless lib makes this much harder? 😅
45- # '$exception_personURL': f'{self.client.posthog_host}/project/{self.client.token}/person/{self.client.get_distinct_id()}'
89+ "$exception_type" : all_exceptions_with_trace_and_in_app [0 ].get ("type" ),
90+ "$exception_message" : all_exceptions_with_trace_and_in_app [0 ].get ("value" ),
91+ "$exception_list" : all_exceptions_with_trace_and_in_app ,
92+ "$exception_personURL" : f"{ remove_trailing_slash (self .client .raw_host )} /project/{ self .client .api_key } /person/{ distinct_id } " ,
4693 }
4794
4895 # TODO: What distinct id should we attach these server-side exceptions to?
4996 # Any heuristic seems prone to errors - how can we know if exception occurred in the context of a user that captured some other event?
5097
51- self .client .capture ("python-exceptions" , "$exception" , properties = properties )
98+ self .client .capture (distinct_id , "$exception" , properties = properties )
5299 except Exception as e :
53100 self .log .exception (f"Failed to capture exception: { e } " )
0 commit comments