Skip to content

Commit 53356e4

Browse files
andrewm4894claude
andcommitted
test: add exception capture integration tests for langchain
Add 6 tests covering the new LLMA + error tracking integration: - capture_exception called on span/generation errors - $exception_event_id added to AI events - No capture when autocapture disabled - AI properties passed to exception event - Handles None return from capture_exception 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d7a23f7 commit 53356e4

1 file changed

Lines changed: 197 additions & 0 deletions

File tree

posthog/test/ai/langchain/test_callbacks.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)