@@ -1807,6 +1807,144 @@ async def test_on_tool_error_callback_logs_correctly(
18071807 assert log_entry ["error_message" ] == "Tool timed out"
18081808 assert log_entry ["status" ] == "ERROR"
18091809
1810+ @pytest .mark .asyncio
1811+ async def test_on_agent_error_callback_logs_correctly (
1812+ self ,
1813+ bq_plugin_inst ,
1814+ mock_write_client ,
1815+ callback_context ,
1816+ mock_agent ,
1817+ dummy_arrow_schema ,
1818+ ):
1819+ """on_agent_error_callback emits AGENT_ERROR with traceback."""
1820+ error = RuntimeError ("Agent crashed" )
1821+ try :
1822+ raise error
1823+ except RuntimeError :
1824+ pass # populate __traceback__
1825+ bigquery_agent_analytics_plugin .TraceManager .push_span (callback_context )
1826+ await bq_plugin_inst .on_agent_error_callback (
1827+ agent = mock_agent ,
1828+ callback_context = callback_context ,
1829+ error = error ,
1830+ )
1831+ await asyncio .sleep (0.05 )
1832+ rows = await _get_captured_rows_async (mock_write_client , dummy_arrow_schema )
1833+ log_entry = next (r for r in rows if r ["event_type" ] == "AGENT_ERROR" )
1834+ assert log_entry ["error_message" ] == "Agent crashed"
1835+ assert log_entry ["status" ] == "ERROR"
1836+ content = json .loads (log_entry ["content" ])
1837+ assert "error_traceback" in content
1838+ assert "RuntimeError: Agent crashed" in content ["error_traceback" ]
1839+
1840+ @pytest .mark .asyncio
1841+ async def test_on_run_error_callback_logs_correctly (
1842+ self ,
1843+ bq_plugin_inst ,
1844+ mock_write_client ,
1845+ invocation_context ,
1846+ dummy_arrow_schema ,
1847+ ):
1848+ """on_run_error_callback emits INVOCATION_ERROR with traceback."""
1849+ error = ValueError ("Invocation failed" )
1850+ try :
1851+ raise error
1852+ except ValueError :
1853+ pass
1854+ bigquery_agent_analytics_plugin .TraceManager .push_span (invocation_context )
1855+ await bq_plugin_inst .on_run_error_callback (
1856+ invocation_context = invocation_context ,
1857+ error = error ,
1858+ )
1859+ await asyncio .sleep (0.05 )
1860+ rows = await _get_captured_rows_async (mock_write_client , dummy_arrow_schema )
1861+ log_entry = next (r for r in rows if r ["event_type" ] == "INVOCATION_ERROR" )
1862+ assert log_entry ["error_message" ] == "Invocation failed"
1863+ assert log_entry ["status" ] == "ERROR"
1864+ content = json .loads (log_entry ["content" ])
1865+ assert "error_traceback" in content
1866+ assert "ValueError: Invocation failed" in content ["error_traceback" ]
1867+
1868+ @pytest .mark .asyncio
1869+ async def test_on_run_error_callback_cleanup_runs_on_log_failure (
1870+ self ,
1871+ bq_plugin_inst ,
1872+ mock_write_client ,
1873+ invocation_context ,
1874+ ):
1875+ """on_run_error_callback cleans up even when _log_event raises."""
1876+ # Push spans and set context vars to simulate active invocation
1877+ bigquery_agent_analytics_plugin .TraceManager .push_span (invocation_context )
1878+ bigquery_agent_analytics_plugin ._active_invocation_id_ctx .set ("test-inv" )
1879+ bigquery_agent_analytics_plugin ._root_agent_name_ctx .set ("test-agent" )
1880+
1881+ # Make _log_event raise
1882+ with mock .patch .object (
1883+ bq_plugin_inst , "_log_event" , side_effect = RuntimeError ("boom" )
1884+ ):
1885+ # @_safe_callback swallows the exception
1886+ await bq_plugin_inst .on_run_error_callback (
1887+ invocation_context = invocation_context ,
1888+ error = ValueError ("app error" ),
1889+ )
1890+
1891+ # finally block must have cleaned up
1892+ assert (
1893+ bigquery_agent_analytics_plugin ._active_invocation_id_ctx .get (None )
1894+ is None
1895+ )
1896+ assert (
1897+ bigquery_agent_analytics_plugin ._root_agent_name_ctx .get (None ) is None
1898+ )
1899+
1900+ @pytest .mark .asyncio
1901+ async def test_traceback_not_truncated_with_negative_max_len (
1902+ self ,
1903+ mock_auth_default ,
1904+ mock_bq_client ,
1905+ mock_write_client ,
1906+ mock_to_arrow_schema ,
1907+ mock_asyncio_to_thread ,
1908+ invocation_context ,
1909+ mock_agent ,
1910+ dummy_arrow_schema ,
1911+ ):
1912+ """Traceback is not truncated when max_content_length is -1."""
1913+ config = bigquery_agent_analytics_plugin .BigQueryLoggerConfig (
1914+ max_content_length = - 1 ,
1915+ create_views = False ,
1916+ )
1917+ async with managed_plugin (
1918+ project_id = PROJECT_ID ,
1919+ dataset_id = DATASET_ID ,
1920+ table_id = TABLE_ID ,
1921+ config = config ,
1922+ ) as plugin :
1923+ await plugin ._ensure_started ()
1924+
1925+ error = RuntimeError ("x" * 2000 )
1926+ try :
1927+ raise error
1928+ except RuntimeError :
1929+ pass
1930+ bigquery_agent_analytics_plugin .TraceManager .push_span (invocation_context )
1931+ await plugin .on_agent_error_callback (
1932+ agent = mock_agent ,
1933+ callback_context = bigquery_agent_analytics_plugin .CallbackContext (
1934+ invocation_context
1935+ ),
1936+ error = error ,
1937+ )
1938+ await asyncio .sleep (0.05 )
1939+ rows = await _get_captured_rows_async (
1940+ mock_write_client , dummy_arrow_schema
1941+ )
1942+ log_entry = next (r for r in rows if r ["event_type" ] == "AGENT_ERROR" )
1943+ content = json .loads (log_entry ["content" ])
1944+ # Should NOT be truncated
1945+ assert "[truncated]" not in content ["error_traceback" ]
1946+ assert "x" * 2000 in content ["error_traceback" ]
1947+
18101948 @pytest .mark .asyncio
18111949 async def test_table_creation_options (
18121950 self ,
@@ -5733,6 +5871,27 @@ def test_view_sql_contains_correct_event_filter(self):
57335871 view_name = "v_" + event_type .lower ()
57345872 assert view_name in all_sql , f"View { view_name } not found in SQL"
57355873
5874+ def test_error_views_contain_traceback_column (self ):
5875+ """AGENT_ERROR and INVOCATION_ERROR views include error_traceback."""
5876+ plugin = self ._make_plugin (create_views = True )
5877+ plugin .client .get_table .side_effect = cloud_exceptions .NotFound ("not found" )
5878+ mock_query_job = mock .MagicMock ()
5879+ plugin .client .query .return_value = mock_query_job
5880+
5881+ plugin ._ensure_schema_exists ()
5882+
5883+ calls = plugin .client .query .call_args_list
5884+ all_sqls = {c [0 ][0 ] for c in calls }
5885+
5886+ agent_error_sqls = [s for s in all_sqls if "v_agent_error" in s ]
5887+ assert len (agent_error_sqls ) == 1
5888+ assert "error_traceback" in agent_error_sqls [0 ]
5889+ assert "total_ms" in agent_error_sqls [0 ]
5890+
5891+ inv_error_sqls = [s for s in all_sqls if "v_invocation_error" in s ]
5892+ assert len (inv_error_sqls ) == 1
5893+ assert "error_traceback" in inv_error_sqls [0 ]
5894+
57365895 def test_config_create_views_default_true (self ):
57375896 """Config create_views defaults to True."""
57385897 config = bigquery_agent_analytics_plugin .BigQueryLoggerConfig ()
0 commit comments