From c44290a9c06f3f23e9c75f66c78b632d5721497d Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Thu, 18 Dec 2025 12:55:34 -0800 Subject: [PATCH] add more error context --- crates/goose-server/src/routes/agent.rs | 19 +++++++++++++++---- crates/goose-server/src/routes/recipe.rs | 4 ++++ crates/goose-server/src/routes/session.rs | 3 +++ crates/goose/src/agents/agent.rs | 9 +++++++-- crates/goose/src/agents/retry.rs | 4 ++++ crates/goose/src/posthog.rs | 15 +++++++++++++-- crates/goose/src/scheduler.rs | 5 ++++- ui/desktop/src/App.tsx | 12 +++++++++++- 8 files changed, 61 insertions(+), 10 deletions(-) diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 8911fbd94b06..f0bd868db575 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -147,6 +147,7 @@ async fn start_agent( Ok(recipe) => Some(recipe), Err(err) => { error!("Failed to decode recipe deeplink: {}", err); + goose::posthog::emit_error("recipe_deeplink_decode_failed", &err.to_string()); return Err(ErrorResponse { message: err.to_string(), status: StatusCode::BAD_REQUEST, @@ -179,6 +180,7 @@ async fn start_agent( .await .map_err(|err| { error!("Failed to create session: {}", err); + goose::posthog::emit_error("session_create_failed", &err.to_string()); ErrorResponse { message: format!("Failed to create session: {}", err), status: StatusCode::BAD_REQUEST, @@ -233,6 +235,7 @@ async fn resume_agent( .await .map_err(|err| { error!("Failed to resume session {}: {}", payload.session_id, err); + goose::posthog::emit_error("session_resume_failed", &err.to_string()); ErrorResponse { message: format!("Failed to resume session: {}", err), status: StatusCode::NOT_FOUND, @@ -304,6 +307,10 @@ async fn resume_agent( async move { if let Err(e) = agent_ref.add_extension(config_clone.clone()).await { warn!("Failed to load extension {}: {}", config_clone.name(), e); + goose::posthog::emit_error( + "extension_load_failed", + &format!("{}: {}", config_clone.name(), e), + ); } Ok::<_, ErrorResponse>(()) } @@ -536,11 +543,15 @@ async fn agent_add_extension( State(state): State>, Json(request): Json, ) -> Result { + let extension_name = request.config.name(); let agent = state.get_agent(request.session_id).await?; - agent - .add_extension(request.config) - .await - .map_err(|e| ErrorResponse::internal(format!("Failed to add extension: {}", e)))?; + agent.add_extension(request.config).await.map_err(|e| { + goose::posthog::emit_error( + "extension_add_failed", + &format!("{}: {}", extension_name, e), + ); + ErrorResponse::internal(format!("Failed to add extension: {}", e)) + })?; Ok(StatusCode::OK) } diff --git a/crates/goose-server/src/routes/recipe.rs b/crates/goose-server/src/routes/recipe.rs index f45ee72ae37a..f35d18d8d053 100644 --- a/crates/goose-server/src/routes/recipe.rs +++ b/crates/goose-server/src/routes/recipe.rs @@ -198,6 +198,7 @@ async fn create_recipe( } Err(e) => { tracing::error!("Error details: {:?}", e); + goose::posthog::emit_error("recipe_create_failed", &e.to_string()); let error_response = CreateRecipeResponse { recipe: None, error: Some(format!("Failed to create recipe: {}", e)), @@ -224,6 +225,7 @@ async fn encode_recipe( Ok(encoded) => Ok(Json(EncodeRecipeResponse { deeplink: encoded })), Err(err) => { tracing::error!("Failed to encode recipe: {}", err); + goose::posthog::emit_error("recipe_encode_failed", &err.to_string()); Err(StatusCode::BAD_REQUEST) } } @@ -249,6 +251,7 @@ async fn decode_recipe( }, Err(err) => { tracing::error!("Failed to decode deeplink: {}", err); + goose::posthog::emit_error("recipe_decode_failed", &err.to_string()); Err(StatusCode::BAD_REQUEST) } } @@ -374,6 +377,7 @@ async fn schedule_recipe( Ok(_) => Ok(StatusCode::OK), Err(e) => { tracing::error!("Failed to schedule recipe: {}", e); + goose::posthog::emit_error("recipe_schedule_failed", &e.to_string()); Err(StatusCode::INTERNAL_SERVER_ERROR) } } diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index 39cc203970e3..09dc95b1f3a7 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -361,6 +361,7 @@ async fn edit_message( .await .map_err(|e| { tracing::error!("Failed to copy session: {}", e); + goose::posthog::emit_error("session_copy_failed", &e.to_string()); StatusCode::INTERNAL_SERVER_ERROR })?; @@ -368,6 +369,7 @@ async fn edit_message( .await .map_err(|e| { tracing::error!("Failed to truncate conversation: {}", e); + goose::posthog::emit_error("session_truncate_failed", &e.to_string()); StatusCode::INTERNAL_SERVER_ERROR })?; @@ -380,6 +382,7 @@ async fn edit_message( .await .map_err(|e| { tracing::error!("Failed to truncate conversation: {}", e); + goose::posthog::emit_error("session_truncate_failed", &e.to_string()); StatusCode::INTERNAL_SERVER_ERROR })?; diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 74a4942a1a69..412902cc0fa1 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -534,6 +534,10 @@ impl Agent { .dispatch_tool_call(tool_call.clone(), cancellation_token.unwrap_or_default()) .await; result.unwrap_or_else(|e| { + crate::posthog::emit_error( + "tool_execution_failed", + &format!("{}: {}", tool_call.name, e), + ); ToolCallResult::from(Err(ErrorData::new( ErrorCode::INTERNAL_ERROR, e.to_string(), @@ -1242,7 +1246,7 @@ impl Agent { } } Err(ref provider_err @ ProviderError::ContextLengthExceeded(_)) => { - crate::posthog::emit_error(provider_err.telemetry_type()); + crate::posthog::emit_error(provider_err.telemetry_type(), &provider_err.to_string()); compaction_attempts += 1; if compaction_attempts >= 2 { @@ -1279,13 +1283,14 @@ impl Agent { break; } Err(e) => { + crate::posthog::emit_error("compaction_failed", &e.to_string()); error!("Compaction failed: {}", e); break; } } } Err(ref provider_err) => { - crate::posthog::emit_error(provider_err.telemetry_type()); + crate::posthog::emit_error(provider_err.telemetry_type(), &provider_err.to_string()); error!("Error: {}", provider_err); yield AgentEvent::Message( Message::assistant().with_text( diff --git a/crates/goose/src/agents/retry.rs b/crates/goose/src/agents/retry.rs index b39dbcfd2cf6..f185c0f55eae 100644 --- a/crates/goose/src/agents/retry.rs +++ b/crates/goose/src/agents/retry.rs @@ -137,6 +137,10 @@ impl RetryManager { "Maximum retry attempts ({}) exceeded", retry_config.max_retries ); + crate::posthog::emit_error( + "retry_max_exceeded", + &format!("Max retries ({}) exceeded", retry_config.max_retries), + ); return Ok(RetryResult::MaxAttemptsReached); } diff --git a/crates/goose/src/posthog.rs b/crates/goose/src/posthog.rs index 73548312874d..d009844396a1 100644 --- a/crates/goose/src/posthog.rs +++ b/crates/goose/src/posthog.rs @@ -218,10 +218,17 @@ pub fn emit_session_started() { pub struct ErrorContext { pub component: Option, pub action: Option, + pub error_message: Option, } -pub fn emit_error(error_type: &str) { - emit_error_with_context(error_type, ErrorContext::default()); +pub fn emit_error(error_type: &str, error_message: &str) { + emit_error_with_context( + error_type, + ErrorContext { + error_message: Some(error_message.to_string()), + ..Default::default() + }, + ); } pub fn emit_error_with_context(error_type: &str, context: ErrorContext) { @@ -273,6 +280,10 @@ async fn send_error_event( if let Some(action) = &context.action { event.insert_prop("action", action.as_str()).ok(); } + if let Some(error_message) = &context.error_message { + let sanitized = sanitize_string(error_message); + event.insert_prop("error_message", sanitized).ok(); + } if let Some(platform_version) = get_platform_version() { event.insert_prop("platform_version", platform_version).ok(); diff --git a/crates/goose/src/scheduler.rs b/crates/goose/src/scheduler.rs index e9ea8bc27d0b..2a21a9484da2 100644 --- a/crates/goose/src/scheduler.rs +++ b/crates/goose/src/scheduler.rs @@ -260,7 +260,10 @@ impl Scheduler { match result { Ok(_) => tracing::info!("Job '{}' completed", task_job_id), - Err(e) => tracing::error!("Job '{}' failed: {}", task_job_id, e), + Err(ref e) => { + tracing::error!("Job '{}' failed: {}", task_job_id, e); + crate::posthog::emit_error("scheduler_job_failed", &e.to_string()); + } } }) }) diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 9170e4f52640..766e4afc9fab 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -43,7 +43,7 @@ import { NoProviderOrModelError, useAgent } from './hooks/useAgent'; import { useNavigation } from './hooks/useNavigation'; import { errorMessage } from './utils/conversionUtils'; import { usePageViewTracking } from './hooks/useAnalytics'; -import { trackOnboardingCompleted } from './utils/analytics'; +import { trackOnboardingCompleted, trackErrorWithContext } from './utils/analytics'; function PageViewTracker() { usePageViewTracking(); @@ -129,6 +129,11 @@ const PairRouteWrapper = ({ setActiveSessionId(newSession.id); } catch (error) { console.error('[PairRouteWrapper] Failed to create session:', error); + trackErrorWithContext(error, { + component: 'PairRouteWrapper', + action: 'create_session', + recoverable: true, + }); } finally { setIsCreatingSession(false); } @@ -428,6 +433,11 @@ export function AppInner() { }); } catch (error) { console.error('Unexpected error opening shared session:', error); + trackErrorWithContext(error, { + component: 'AppInner', + action: 'open_shared_session', + recoverable: true, + }); // Navigate to shared session view with error const shareToken = link.replace('goose://sessions/', ''); const options = {