@@ -958,3 +958,291 @@ def test_langchain_message_role_normalization_units():
958958 assert normalized [3 ]["role" ] == "system" # system unchanged
959959 assert "role" not in normalized [4 ] # Message without role unchanged
960960 assert normalized [5 ] == "string message" # String message unchanged
961+
962+
963+ def test_langchain_llm_exception_captured (sentry_init , capture_events ):
964+ """Test that exceptions during LLM execution are properly captured with full context."""
965+ global llm_type
966+ llm_type = "openai-chat"
967+
968+ sentry_init (
969+ integrations = [LangchainIntegration (include_prompts = True )],
970+ traces_sample_rate = 1.0 ,
971+ send_default_pii = True ,
972+ )
973+ events = capture_events ()
974+
975+ prompt = ChatPromptTemplate .from_messages (
976+ [
977+ ("system" , "You are a helpful assistant" ),
978+ ("user" , "{input}" ),
979+ MessagesPlaceholder (variable_name = "agent_scratchpad" ),
980+ ]
981+ )
982+
983+ global stream_result_mock
984+ stream_result_mock = Mock (side_effect = RuntimeError ("LLM service unavailable" ))
985+
986+ llm = MockOpenAI (
987+ model_name = "gpt-3.5-turbo" ,
988+ temperature = 0 ,
989+ openai_api_key = "badkey" ,
990+ )
991+ agent = create_openai_tools_agent (llm , [get_word_length ], prompt )
992+ agent_executor = AgentExecutor (agent = agent , tools = [get_word_length ], verbose = True )
993+
994+ with start_transaction (name = "test_llm_exception" ):
995+ with pytest .raises (RuntimeError ):
996+ list (agent_executor .stream ({"input" : "Test input" }))
997+
998+ (error_event , transaction_event ) = events
999+
1000+ assert error_event ["level" ] == "error"
1001+ assert "exception" in error_event
1002+ assert len (error_event ["exception" ]["values" ]) > 0
1003+
1004+ exception = error_event ["exception" ]["values" ][0 ]
1005+ assert exception ["type" ] == "RuntimeError"
1006+ assert exception ["value" ] == "LLM service unavailable"
1007+ assert "stacktrace" in exception
1008+
1009+ assert transaction_event ["type" ] == "transaction"
1010+ assert transaction_event ["transaction" ] == "test_llm_exception"
1011+ assert transaction_event ["contexts" ]["trace" ]["status" ] == "error"
1012+
1013+
1014+ def test_langchain_different_exception_types (sentry_init , capture_events ):
1015+ """Test that different exception types are properly captured."""
1016+ global llm_type
1017+ llm_type = "openai-chat"
1018+
1019+ exception_types = [
1020+ (ValueError , "Invalid parameter" ),
1021+ (TypeError , "Type mismatch" ),
1022+ (RuntimeError , "Runtime error occurred" ),
1023+ (Exception , "Generic exception" ),
1024+ ]
1025+
1026+ for exception_class , exception_message in exception_types :
1027+ sentry_init (
1028+ integrations = [LangchainIntegration (include_prompts = False )],
1029+ traces_sample_rate = 1.0 ,
1030+ )
1031+ events = capture_events ()
1032+
1033+ prompt = ChatPromptTemplate .from_messages (
1034+ [
1035+ ("system" , "You are a helpful assistant" ),
1036+ ("user" , "{input}" ),
1037+ MessagesPlaceholder (variable_name = "agent_scratchpad" ),
1038+ ]
1039+ )
1040+
1041+ global stream_result_mock
1042+ stream_result_mock = Mock (side_effect = exception_class (exception_message ))
1043+
1044+ llm = MockOpenAI (
1045+ model_name = "gpt-3.5-turbo" ,
1046+ temperature = 0 ,
1047+ openai_api_key = "badkey" ,
1048+ )
1049+ agent = create_openai_tools_agent (llm , [get_word_length ], prompt )
1050+ agent_executor = AgentExecutor (
1051+ agent = agent , tools = [get_word_length ], verbose = True
1052+ )
1053+
1054+ with start_transaction ():
1055+ with pytest .raises (exception_class ):
1056+ list (agent_executor .stream ({"input" : "Test" }))
1057+
1058+ assert len (events ) >= 1
1059+ error_event = events [0 ]
1060+ assert error_event ["level" ] == "error"
1061+
1062+ exception = error_event ["exception" ]["values" ][0 ]
1063+ assert exception ["type" ] == exception_class .__name__
1064+ assert exception ["value" ] == exception_message
1065+
1066+
1067+ def test_langchain_exception_with_span_context (sentry_init , capture_events ):
1068+ """Test that exception events include proper span context."""
1069+ global llm_type
1070+ llm_type = "openai-chat"
1071+
1072+ sentry_init (
1073+ integrations = [LangchainIntegration (include_prompts = True )],
1074+ traces_sample_rate = 1.0 ,
1075+ send_default_pii = True ,
1076+ )
1077+ events = capture_events ()
1078+
1079+ prompt = ChatPromptTemplate .from_messages (
1080+ [
1081+ ("system" , "You are a helpful assistant" ),
1082+ ("user" , "{input}" ),
1083+ MessagesPlaceholder (variable_name = "agent_scratchpad" ),
1084+ ]
1085+ )
1086+
1087+ global stream_result_mock
1088+ stream_result_mock = Mock (side_effect = ValueError ("Model error" ))
1089+
1090+ llm = MockOpenAI (
1091+ model_name = "gpt-4" ,
1092+ temperature = 0.7 ,
1093+ openai_api_key = "badkey" ,
1094+ )
1095+ agent = create_openai_tools_agent (llm , [get_word_length ], prompt )
1096+ agent_executor = AgentExecutor (agent = agent , tools = [get_word_length ], verbose = True )
1097+
1098+ with start_transaction (name = "llm_with_error" ):
1099+ with pytest .raises (ValueError ):
1100+ list (agent_executor .stream ({"input" : "Cause an error" }))
1101+
1102+ error_event , transaction_event = events
1103+
1104+ assert "contexts" in error_event
1105+ assert "trace" in error_event ["contexts" ]
1106+
1107+ error_trace_id = error_event ["contexts" ]["trace" ].get ("trace_id" )
1108+ transaction_trace_id = transaction_event ["contexts" ]["trace" ]["trace_id" ]
1109+
1110+ assert error_trace_id == transaction_trace_id
1111+
1112+ gen_ai_spans = [
1113+ span
1114+ for span in transaction_event .get ("spans" , [])
1115+ if span .get ("op" , "" ).startswith ("gen_ai" )
1116+ ]
1117+ assert len (gen_ai_spans ) > 0
1118+
1119+ for span in gen_ai_spans :
1120+ if span .get ("tags" , {}).get ("status" ) == "error" :
1121+ assert "span_id" in span
1122+
1123+
1124+ def test_langchain_tool_execution_error (sentry_init , capture_events ):
1125+ """Test that exceptions during tool execution are properly captured."""
1126+ global llm_type
1127+ llm_type = "openai-chat"
1128+
1129+ sentry_init (
1130+ integrations = [LangchainIntegration (include_prompts = True )],
1131+ traces_sample_rate = 1.0 ,
1132+ send_default_pii = True ,
1133+ )
1134+ events = capture_events ()
1135+
1136+ @tool
1137+ def failing_tool (word : str ) -> int :
1138+ """A tool that always fails."""
1139+ raise RuntimeError ("Tool execution failed" )
1140+
1141+ prompt = ChatPromptTemplate .from_messages (
1142+ [
1143+ ("system" , "You are a helpful assistant" ),
1144+ ("user" , "{input}" ),
1145+ MessagesPlaceholder (variable_name = "agent_scratchpad" ),
1146+ ]
1147+ )
1148+
1149+ global stream_result_mock
1150+ stream_result_mock = Mock (
1151+ side_effect = [
1152+ [
1153+ ChatGenerationChunk (
1154+ type = "ChatGenerationChunk" ,
1155+ message = AIMessageChunk (
1156+ content = "" ,
1157+ additional_kwargs = {
1158+ "tool_calls" : [
1159+ {
1160+ "index" : 0 ,
1161+ "id" : "call_test" ,
1162+ "function" : {
1163+ "arguments" : '{"word": "test"}' ,
1164+ "name" : "failing_tool" ,
1165+ },
1166+ "type" : "function" ,
1167+ }
1168+ ]
1169+ },
1170+ ),
1171+ ),
1172+ ]
1173+ ]
1174+ )
1175+
1176+ llm = MockOpenAI (
1177+ model_name = "gpt-3.5-turbo" ,
1178+ temperature = 0 ,
1179+ openai_api_key = "badkey" ,
1180+ )
1181+ agent = create_openai_tools_agent (llm , [failing_tool ], prompt )
1182+ agent_executor = AgentExecutor (agent = agent , tools = [failing_tool ], verbose = True )
1183+
1184+ with start_transaction ():
1185+ with pytest .raises (RuntimeError ):
1186+ list (agent_executor .stream ({"input" : "Use the failing tool" }))
1187+
1188+ assert len (events ) >= 1
1189+
1190+ error_events = [e for e in events if e .get ("level" ) == "error" ]
1191+ assert len (error_events ) > 0
1192+
1193+ error_event = error_events [0 ]
1194+ exception = error_event ["exception" ]["values" ][0 ]
1195+ assert exception ["type" ] == "RuntimeError"
1196+ assert "Tool execution failed" in exception ["value" ]
1197+
1198+
1199+ def test_langchain_exception_span_cleanup (sentry_init , capture_events ):
1200+ """Test that spans are properly cleaned up even when exceptions occur."""
1201+ global llm_type
1202+ llm_type = "openai-chat"
1203+
1204+ sentry_init (
1205+ integrations = [LangchainIntegration (include_prompts = True )],
1206+ traces_sample_rate = 1.0 ,
1207+ )
1208+ events = capture_events ()
1209+
1210+ prompt = ChatPromptTemplate .from_messages (
1211+ [
1212+ ("system" , "You are a helpful assistant" ),
1213+ ("user" , "{input}" ),
1214+ MessagesPlaceholder (variable_name = "agent_scratchpad" ),
1215+ ]
1216+ )
1217+
1218+ global stream_result_mock
1219+ stream_result_mock = Mock (side_effect = ValueError ("Test error" ))
1220+
1221+ llm = MockOpenAI (
1222+ model_name = "gpt-3.5-turbo" ,
1223+ temperature = 0 ,
1224+ openai_api_key = "badkey" ,
1225+ )
1226+ agent = create_openai_tools_agent (llm , [get_word_length ], prompt )
1227+ agent_executor = AgentExecutor (agent = agent , tools = [get_word_length ], verbose = True )
1228+
1229+ with start_transaction ():
1230+ with pytest .raises (ValueError ):
1231+ list (agent_executor .stream ({"input" : "Test" }))
1232+
1233+ transaction_event = next (
1234+ (e for e in events if e .get ("type" ) == "transaction" ), None
1235+ )
1236+ assert transaction_event is not None
1237+
1238+ errored_spans = [
1239+ span
1240+ for span in transaction_event .get ("spans" , [])
1241+ if span .get ("tags" , {}).get ("status" ) == "error"
1242+ ]
1243+
1244+ assert len (errored_spans ) > 0
1245+
1246+ for span in errored_spans :
1247+ assert "timestamp" in span
1248+ assert span ["timestamp" ] > span .get ("start_timestamp" , 0 )
0 commit comments