@@ -2441,3 +2441,200 @@ def test_billable_with_real_chain(mock_client):
24412441 assert props ["$ai_billable" ] is True
24422442 assert props ["$ai_model" ] == "fake-model"
24432443 assert props ["$ai_provider" ] == "fake"
2444+
2445+
2446+ # Exception Capture Integration Tests
2447+
2448+
2449+ def test_exception_autocapture_on_span_error ():
2450+ """Test that capture_exception is called when a span errors and autocapture is enabled."""
2451+ mock_client = MagicMock ()
2452+ mock_client .privacy_mode = False
2453+ mock_client .enable_exception_autocapture = True
2454+ mock_client .capture_exception .return_value = "exception-uuid-123"
2455+
2456+ def failing_span (_ ):
2457+ raise ValueError ("test error" )
2458+
2459+ callbacks = [CallbackHandler (mock_client )]
2460+ chain = RunnableLambda (failing_span )
2461+
2462+ try :
2463+ chain .invoke ({}, config = {"callbacks" : callbacks })
2464+ except ValueError :
2465+ pass
2466+
2467+ # Verify capture_exception was called
2468+ assert mock_client .capture_exception .call_count == 1
2469+ exception_call = mock_client .capture_exception .call_args
2470+ assert isinstance (exception_call [0 ][0 ], ValueError )
2471+ assert str (exception_call [0 ][0 ]) == "test error"
2472+
2473+
2474+ def test_exception_autocapture_adds_exception_id_to_span_event ():
2475+ """Test that $exception_event_id is added to the span event properties."""
2476+ mock_client = MagicMock ()
2477+ mock_client .privacy_mode = False
2478+ mock_client .enable_exception_autocapture = True
2479+ mock_client .capture_exception .return_value = "exception-uuid-456"
2480+
2481+ def failing_span (_ ):
2482+ raise ValueError ("test error" )
2483+
2484+ callbacks = [CallbackHandler (mock_client )]
2485+ chain = RunnableLambda (failing_span )
2486+
2487+ try :
2488+ chain .invoke ({}, config = {"callbacks" : callbacks })
2489+ except ValueError :
2490+ pass
2491+
2492+ # Find the span event (should have $ai_is_error=True)
2493+ span_calls = [
2494+ call for call in mock_client .capture .call_args_list
2495+ if call [1 ].get ("properties" , {}).get ("$ai_is_error" ) is True
2496+ ]
2497+ assert len (span_calls ) >= 1
2498+
2499+ span_props = span_calls [0 ][1 ]["properties" ]
2500+ assert span_props ["$exception_event_id" ] == "exception-uuid-456"
2501+ assert span_props ["$ai_error" ] == "ValueError: test error"
2502+
2503+
2504+ def test_exception_autocapture_disabled_does_not_capture ():
2505+ """Test that capture_exception is NOT called when autocapture is disabled."""
2506+ mock_client = MagicMock ()
2507+ mock_client .privacy_mode = False
2508+ mock_client .enable_exception_autocapture = False
2509+
2510+ def failing_span (_ ):
2511+ raise ValueError ("test error" )
2512+
2513+ callbacks = [CallbackHandler (mock_client )]
2514+ chain = RunnableLambda (failing_span )
2515+
2516+ try :
2517+ chain .invoke ({}, config = {"callbacks" : callbacks })
2518+ except ValueError :
2519+ pass
2520+
2521+ # Verify capture_exception was NOT called
2522+ assert mock_client .capture_exception .call_count == 0
2523+
2524+ # But the span event should still have error info
2525+ span_calls = [
2526+ call for call in mock_client .capture .call_args_list
2527+ if call [1 ].get ("properties" , {}).get ("$ai_is_error" ) is True
2528+ ]
2529+ assert len (span_calls ) >= 1
2530+
2531+ span_props = span_calls [0 ][1 ]["properties" ]
2532+ assert "$exception_event_id" not in span_props
2533+ assert span_props ["$ai_error" ] == "ValueError: test error"
2534+
2535+
2536+ def test_exception_autocapture_on_llm_generation_error (mock_client ):
2537+ """Test that capture_exception is called when an LLM generation fails."""
2538+ mock_client .privacy_mode = False
2539+ mock_client .enable_exception_autocapture = True
2540+ mock_client .capture_exception .return_value = "exception-uuid-789"
2541+
2542+ callbacks = CallbackHandler (mock_client )
2543+ run_id = uuid .uuid4 ()
2544+
2545+ # Simulate LLM start
2546+ callbacks .on_llm_start (
2547+ serialized = {"kwargs" : {"openai_api_base" : "https://api.openai.com" }},
2548+ prompts = ["Hello" ],
2549+ run_id = run_id ,
2550+ )
2551+
2552+ # Simulate LLM error
2553+ error = Exception ("API rate limit exceeded" )
2554+ callbacks .on_llm_error (error , run_id = run_id )
2555+
2556+ # Verify capture_exception was called
2557+ assert mock_client .capture_exception .call_count == 1
2558+ exception_call = mock_client .capture_exception .call_args
2559+ assert exception_call [0 ][0 ] is error
2560+
2561+ # Verify the generation event has $exception_event_id
2562+ generation_calls = [
2563+ call for call in mock_client .capture .call_args_list
2564+ if call [1 ].get ("event" ) == "$ai_generation"
2565+ ]
2566+ assert len (generation_calls ) == 1
2567+
2568+ gen_props = generation_calls [0 ][1 ]["properties" ]
2569+ assert gen_props ["$exception_event_id" ] == "exception-uuid-789"
2570+ assert gen_props ["$ai_is_error" ] is True
2571+
2572+
2573+ def test_exception_autocapture_passes_ai_properties_to_exception ():
2574+ """Test that AI properties are passed to the exception event."""
2575+ mock_client = MagicMock ()
2576+ mock_client .privacy_mode = False
2577+ mock_client .enable_exception_autocapture = True
2578+ mock_client .capture_exception .return_value = "exception-uuid-abc"
2579+
2580+ callbacks = CallbackHandler (
2581+ mock_client ,
2582+ distinct_id = "user-123" ,
2583+ properties = {"custom_prop" : "custom_value" },
2584+ )
2585+ run_id = uuid .uuid4 ()
2586+
2587+ # Simulate LLM start
2588+ callbacks .on_llm_start (
2589+ serialized = {"kwargs" : {"openai_api_base" : "https://api.openai.com" }},
2590+ prompts = ["Hello" ],
2591+ run_id = run_id ,
2592+ )
2593+
2594+ # Simulate LLM error
2595+ error = Exception ("API error" )
2596+ callbacks .on_llm_error (error , run_id = run_id )
2597+
2598+ # Verify capture_exception received the properties
2599+ exception_call = mock_client .capture_exception .call_args
2600+ props = exception_call [1 ]["properties" ]
2601+
2602+ # Should have AI-related properties
2603+ assert "$ai_trace_id" in props
2604+ assert "$ai_is_error" in props
2605+ assert props ["$ai_is_error" ] is True
2606+
2607+ # Should have distinct_id passed through
2608+ assert exception_call [1 ]["distinct_id" ] == "user-123"
2609+
2610+
2611+ def test_exception_autocapture_none_return_no_exception_id ():
2612+ """Test that when capture_exception returns None, no $exception_event_id is added."""
2613+ mock_client = MagicMock ()
2614+ mock_client .privacy_mode = False
2615+ mock_client .enable_exception_autocapture = True
2616+ mock_client .capture_exception .return_value = None # e.g., exception already captured
2617+
2618+ def failing_span (_ ):
2619+ raise ValueError ("test error" )
2620+
2621+ callbacks = [CallbackHandler (mock_client )]
2622+ chain = RunnableLambda (failing_span )
2623+
2624+ try :
2625+ chain .invoke ({}, config = {"callbacks" : callbacks })
2626+ except ValueError :
2627+ pass
2628+
2629+ # capture_exception was called but returned None
2630+ assert mock_client .capture_exception .call_count == 1
2631+
2632+ # Span event should NOT have $exception_event_id
2633+ span_calls = [
2634+ call for call in mock_client .capture .call_args_list
2635+ if call [1 ].get ("properties" , {}).get ("$ai_is_error" ) is True
2636+ ]
2637+ assert len (span_calls ) >= 1
2638+
2639+ span_props = span_calls [0 ][1 ]["properties" ]
2640+ assert "$exception_event_id" not in span_props
0 commit comments