3030 from urlparse import urlsplit # type: ignore
3131 from urlparse import urlunsplit # type: ignore
3232
33+ try :
34+ # Python 3.11
35+ from builtins import BaseExceptionGroup
36+ except ImportError :
37+ # Python 3.10 and below
38+ BaseExceptionGroup = None # type: ignore
3339
3440from datetime import datetime
3541from functools import partial
@@ -666,19 +672,54 @@ def single_exception_from_error_tuple(
666672 tb , # type: Optional[TracebackType]
667673 client_options = None , # type: Optional[Dict[str, Any]]
668674 mechanism = None , # type: Optional[Dict[str, Any]]
675+ exception_id = None , # type: Optional[int]
676+ parent_id = None , # type: Optional[int]
677+ source = None , # type: Optional[str]
669678):
670679 # type: (...) -> Dict[str, Any]
671- mechanism = mechanism or {"type" : "generic" , "handled" : True }
680+ """
681+ Creates a dict that goes into the events `exception.values` list and is ingestible by Sentry.
682+
683+ See the Exception Interface documentation for more details:
684+ https://develop.sentry.dev/sdk/event-payloads/exception/
685+ """
686+ exception_value = {} # type: Dict[str, Any]
687+ exception_value ["mechanism" ] = (
688+ mechanism .copy () if mechanism else {"type" : "generic" , "handled" : True }
689+ )
690+ if exception_id is not None :
691+ exception_value ["mechanism" ]["exception_id" ] = exception_id
672692
673693 if exc_value is not None :
674694 errno = get_errno (exc_value )
675695 else :
676696 errno = None
677697
678698 if errno is not None :
679- mechanism .setdefault ("meta" , {}).setdefault ("errno" , {}).setdefault (
680- "number" , errno
681- )
699+ exception_value ["mechanism" ].setdefault ("meta" , {}).setdefault (
700+ "errno" , {}
701+ ).setdefault ("number" , errno )
702+
703+ if source is not None :
704+ exception_value ["mechanism" ]["source" ] = source
705+
706+ is_root_exception = exception_id == 0
707+ if not is_root_exception and parent_id is not None :
708+ exception_value ["mechanism" ]["parent_id" ] = parent_id
709+ exception_value ["mechanism" ]["type" ] = "chained"
710+
711+ if is_root_exception and "type" not in exception_value ["mechanism" ]:
712+ exception_value ["mechanism" ]["type" ] = "generic"
713+
714+ is_exception_group = BaseExceptionGroup is not None and isinstance (
715+ exc_value , BaseExceptionGroup
716+ )
717+ if is_exception_group :
718+ exception_value ["mechanism" ]["is_exception_group" ] = True
719+
720+ exception_value ["module" ] = get_type_module (exc_type )
721+ exception_value ["type" ] = get_type_name (exc_type )
722+ exception_value ["value" ] = getattr (exc_value , "message" , safe_str (exc_value ))
682723
683724 if client_options is None :
684725 include_local_variables = True
@@ -697,17 +738,10 @@ def single_exception_from_error_tuple(
697738 for tb in iter_stacks (tb )
698739 ]
699740
700- rv = {
701- "module" : get_type_module (exc_type ),
702- "type" : get_type_name (exc_type ),
703- "value" : safe_str (exc_value ),
704- "mechanism" : mechanism ,
705- }
706-
707741 if frames :
708- rv ["stacktrace" ] = {"frames" : frames }
742+ exception_value ["stacktrace" ] = {"frames" : frames }
709743
710- return rv
744+ return exception_value
711745
712746
713747HAS_CHAINED_EXCEPTIONS = hasattr (Exception , "__suppress_context__" )
@@ -751,24 +785,139 @@ def walk_exception_chain(exc_info):
751785 yield exc_info
752786
753787
788+ def exceptions_from_error (
789+ exc_type , # type: Optional[type]
790+ exc_value , # type: Optional[BaseException]
791+ tb , # type: Optional[TracebackType]
792+ client_options = None , # type: Optional[Dict[str, Any]]
793+ mechanism = None , # type: Optional[Dict[str, Any]]
794+ exception_id = 0 , # type: int
795+ parent_id = 0 , # type: int
796+ source = None , # type: Optional[str]
797+ ):
798+ # type: (...) -> Tuple[int, List[Dict[str, Any]]]
799+ """
800+ Creates the list of exceptions.
801+ This can include chained exceptions and exceptions from an ExceptionGroup.
802+
803+ See the Exception Interface documentation for more details:
804+ https://develop.sentry.dev/sdk/event-payloads/exception/
805+ """
806+
807+ parent = single_exception_from_error_tuple (
808+ exc_type = exc_type ,
809+ exc_value = exc_value ,
810+ tb = tb ,
811+ client_options = client_options ,
812+ mechanism = mechanism ,
813+ exception_id = exception_id ,
814+ parent_id = parent_id ,
815+ source = source ,
816+ )
817+ exceptions = [parent ]
818+
819+ parent_id = exception_id
820+ exception_id += 1
821+
822+ should_supress_context = (
823+ hasattr (exc_value , "__suppress_context__" ) and exc_value .__suppress_context__ # type: ignore
824+ )
825+ if should_supress_context :
826+ # Add direct cause.
827+ # The field `__cause__` is set when raised with the exception (using the `from` keyword).
828+ exception_has_cause = (
829+ exc_value
830+ and hasattr (exc_value , "__cause__" )
831+ and exc_value .__cause__ is not None
832+ )
833+ if exception_has_cause :
834+ cause = exc_value .__cause__ # type: ignore
835+ (exception_id , child_exceptions ) = exceptions_from_error (
836+ exc_type = type (cause ),
837+ exc_value = cause ,
838+ tb = getattr (cause , "__traceback__" , None ),
839+ client_options = client_options ,
840+ mechanism = mechanism ,
841+ exception_id = exception_id ,
842+ source = "__cause__" ,
843+ )
844+ exceptions .extend (child_exceptions )
845+
846+ else :
847+ # Add indirect cause.
848+ # The field `__context__` is assigned if another exception occurs while handling the exception.
849+ exception_has_content = (
850+ exc_value
851+ and hasattr (exc_value , "__context__" )
852+ and exc_value .__context__ is not None
853+ )
854+ if exception_has_content :
855+ context = exc_value .__context__ # type: ignore
856+ (exception_id , child_exceptions ) = exceptions_from_error (
857+ exc_type = type (context ),
858+ exc_value = context ,
859+ tb = getattr (context , "__traceback__" , None ),
860+ client_options = client_options ,
861+ mechanism = mechanism ,
862+ exception_id = exception_id ,
863+ source = "__context__" ,
864+ )
865+ exceptions .extend (child_exceptions )
866+
867+ # Add exceptions from an ExceptionGroup.
868+ is_exception_group = exc_value and hasattr (exc_value , "exceptions" )
869+ if is_exception_group :
870+ for idx , e in enumerate (exc_value .exceptions ): # type: ignore
871+ (exception_id , child_exceptions ) = exceptions_from_error (
872+ exc_type = type (e ),
873+ exc_value = e ,
874+ tb = getattr (e , "__traceback__" , None ),
875+ client_options = client_options ,
876+ mechanism = mechanism ,
877+ exception_id = exception_id ,
878+ parent_id = parent_id ,
879+ source = "exceptions[%s]" % idx ,
880+ )
881+ exceptions .extend (child_exceptions )
882+
883+ return (exception_id , exceptions )
884+
885+
754886def exceptions_from_error_tuple (
755887 exc_info , # type: ExcInfo
756888 client_options = None , # type: Optional[Dict[str, Any]]
757889 mechanism = None , # type: Optional[Dict[str, Any]]
758890):
759891 # type: (...) -> List[Dict[str, Any]]
760892 exc_type , exc_value , tb = exc_info
761- rv = []
762- for exc_type , exc_value , tb in walk_exception_chain (exc_info ):
763- rv .append (
764- single_exception_from_error_tuple (
765- exc_type , exc_value , tb , client_options , mechanism
766- )
893+
894+ is_exception_group = BaseExceptionGroup is not None and isinstance (
895+ exc_value , BaseExceptionGroup
896+ )
897+
898+ if is_exception_group :
899+ (_ , exceptions ) = exceptions_from_error (
900+ exc_type = exc_type ,
901+ exc_value = exc_value ,
902+ tb = tb ,
903+ client_options = client_options ,
904+ mechanism = mechanism ,
905+ exception_id = 0 ,
906+ parent_id = 0 ,
767907 )
768908
769- rv .reverse ()
909+ else :
910+ exceptions = []
911+ for exc_type , exc_value , tb in walk_exception_chain (exc_info ):
912+ exceptions .append (
913+ single_exception_from_error_tuple (
914+ exc_type , exc_value , tb , client_options , mechanism
915+ )
916+ )
917+
918+ exceptions .reverse ()
770919
771- return rv
920+ return exceptions
772921
773922
774923def to_string (value ):
0 commit comments