From 58c1dbae22302f96ea83d6c8c26eb7446fd71644 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Mon, 10 Aug 2026 11:12:51 +0530 Subject: [PATCH 1/7] chore: reduce PostHog tracking to error-level logs and truncated traces - Ship only error-level logs to PostHog (was info-level) - Truncate trace payloads to 1024 bytes before dispatch --- crates/forge_tracker/src/event.rs | 13 ++++++++++++- crates/forge_tracker/src/log.rs | 3 ++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/forge_tracker/src/event.rs b/crates/forge_tracker/src/event.rs index ed09c919d4..ae078aba0d 100644 --- a/crates/forge_tracker/src/event.rs +++ b/crates/forge_tracker/src/event.rs @@ -70,6 +70,10 @@ impl ToolCallPayload { } } +/// Maximum size (in bytes) of a trace payload sent to collectors. +/// Traces beyond this size are truncated to keep tracking minimal. +const MAX_TRACE_LEN: usize = 1024; + #[derive(Debug, Clone)] pub enum EventKind { Start, @@ -97,7 +101,14 @@ impl EventKind { Self::Prompt(content) => content.to_string(), Self::Error(content) => content.to_string(), Self::ToolCall(payload) => serde_json::to_string(&payload).unwrap_or_default(), - Self::Trace(trace) => trace.to_str_lossy().to_string(), + Self::Trace(trace) => { + let text = trace.to_str_lossy(); + let mut end = text.len().min(MAX_TRACE_LEN); + while !text.is_char_boundary(end) { + end -= 1; + } + text[..end].to_string() + } Self::Login(id) => id.login.to_owned(), } } diff --git a/crates/forge_tracker/src/log.rs b/crates/forge_tracker/src/log.rs index df4cda283d..ad4de2fe24 100644 --- a/crates/forge_tracker/src/log.rs +++ b/crates/forge_tracker/src/log.rs @@ -48,7 +48,8 @@ fn prepare_writer( let append = PostHogWriter::new(tracker); ( tracing_appender::non_blocking(append), - tracing_subscriber::EnvFilter::new("forge=info"), + // Only ship error-level logs to PostHog to keep tracking minimal + tracing_subscriber::EnvFilter::new("forge=error"), ) } else { let append = tracing_appender::rolling::daily(log_path, "forge.log"); From 6f9f19a2d756fd451c007a0c88d9819e1cd0cc86 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Mon, 10 Aug 2026 11:17:39 +0530 Subject: [PATCH 2/7] chore: drop Prompt/ToolCall events and conversation payload from tracking - Remove EventKind::Prompt and EventKind::ToolCall (and ToolCallPayload) - Stop attaching full Conversation objects to tracked events - Keep Start, Error, Login, and (truncated) Trace events --- crates/forge_main/src/input.rs | 2 -- crates/forge_main/src/tracker.rs | 12 +----------- crates/forge_main/src/ui.rs | 20 +------------------- crates/forge_tracker/src/dispatch.rs | 18 +----------------- crates/forge_tracker/src/event.rs | 26 -------------------------- crates/forge_tracker/src/lib.rs | 2 +- 6 files changed, 4 insertions(+), 76 deletions(-) diff --git a/crates/forge_main/src/input.rs b/crates/forge_main/src/input.rs index 3612f858f0..b13eabaa31 100644 --- a/crates/forge_main/src/input.rs +++ b/crates/forge_main/src/input.rs @@ -6,7 +6,6 @@ use forge_api::Environment; use crate::editor::{ForgeEditor, ReadResult}; use crate::model::{AppCommand, ForgeCommandManager}; use crate::prompt::ForgePrompt; -use crate::tracker; /// Console implementation for handling user input via command line. pub struct Console { @@ -38,7 +37,6 @@ impl Console { ReadResult::Exit => return Ok(AppCommand::Exit), ReadResult::Empty => continue, ReadResult::Success(text) => { - tracker::prompt(text.clone()); return self.command.parse(&text); } } diff --git a/crates/forge_main/src/tracker.rs b/crates/forge_main/src/tracker.rs index c033c4541d..0709d4255d 100644 --- a/crates/forge_main/src/tracker.rs +++ b/crates/forge_main/src/tracker.rs @@ -1,4 +1,4 @@ -use forge_tracker::{EventKind, ToolCallPayload}; +use forge_tracker::EventKind; use crate::TRACKER; @@ -32,16 +32,6 @@ pub fn error_string(error: String) { dispatch(EventKind::Error(error)); } -/// For tool call events -pub fn tool_call(payload: ToolCallPayload) { - dispatch(EventKind::ToolCall(payload)); -} - -/// For prompt events -pub fn prompt(text: String) { - dispatch(EventKind::Prompt(text)); -} - /// For model setting pub fn set_model(model: String) { tokio::spawn(TRACKER.set_model(model)); diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index a517907b24..d9b4f3e83e 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -23,7 +23,6 @@ use forge_domain::{ use forge_fs::ForgeFS; use forge_select::{ForgeWidget, SelectRow}; use forge_spinner::SpinnerManager; -use forge_tracker::ToolCallPayload; use forge_walker::Walker; use futures::future; use strum::IntoEnumIterator; @@ -376,7 +375,6 @@ impl A + Send + Sync> UI // Handle direct prompt or piped input if provided (raw text messages) let input = self.cli.prompt.clone().or(self.cli.piped_input.clone()); if let Some(input) = input { - tracker::prompt(input.clone()); self.spinner.start(None)?; tokio::select! { _ = tokio::signal::ctrl_c() => { @@ -408,10 +406,6 @@ impl A + Send + Sync> UI match result { Ok(exit) => if exit {return Ok(())}, Err(error) => { - if let Some(conversation_id) = self.state.conversation_id.as_ref() - && let Some(conversation) = self.api.conversation(conversation_id).await.ok().flatten() { - TRACKER.set_conversation(conversation).await; - } tracker::error(&error); tracing::error!(error = ?error); self.spinner.stop(None)?; @@ -4167,19 +4161,7 @@ impl A + Send + Sync> UI // stdout from appearing before the tool name is printed. drop(_guard); } - ChatResponse::ToolCallEnd(toolcall_result) => { - // Only track toolcall name in case of success else track the error. - let payload = if toolcall_result.is_error() { - let mut r = ToolCallPayload::new(toolcall_result.name.to_string()); - if let Some(cause) = toolcall_result.output.as_str() { - r = r.with_cause(cause.to_string()); - } - r - } else { - ToolCallPayload::new(toolcall_result.name.to_string()) - }; - tracker::tool_call(payload); - + ChatResponse::ToolCallEnd(_toolcall_result) => { self.spinner.start(None)?; if !self.cli.verbose { return Ok(()); diff --git a/crates/forge_tracker/src/dispatch.rs b/crates/forge_tracker/src/dispatch.rs index bbec64e4f3..5c40fcafd0 100644 --- a/crates/forge_tracker/src/dispatch.rs +++ b/crates/forge_tracker/src/dispatch.rs @@ -5,7 +5,6 @@ use std::sync::{Arc, LazyLock}; use bstr::ByteSlice; use chrono::{DateTime, Utc}; -use forge_domain::Conversation; use sysinfo::System; use tokio::process::Command; use tokio::sync::Mutex; @@ -65,7 +64,6 @@ pub struct Tracker { start_time: DateTime, email: Arc>>>, model: Arc>>, - conversation: Arc>>, is_logged_in: Arc, rate_limiter: Arc>, } @@ -81,7 +79,6 @@ impl Default for Tracker { start_time, email: Arc::new(Mutex::new(None)), model: Arc::new(Mutex::new(None)), - conversation: Arc::new(Mutex::new(None)), is_logged_in: Arc::new(AtomicBool::new(false)), rate_limiter: Arc::new(Mutex::new(RateLimiter::new(MAX_EVENTS_PER_MINUTE))), } @@ -131,7 +128,6 @@ impl Tracker { version: version(), email: email.clone(), model: self.model.lock().await.clone(), - conversation: self.conversation().await, identity: match event_kind { EventKind::Login(id) => Some(id), _ => None, @@ -153,15 +149,6 @@ impl Tracker { guard.clone().unwrap_or_default() } - async fn conversation(&self) -> Option { - let mut guard = self.conversation.lock().await; - let conversation = guard.clone(); - *guard = None; - conversation - } - pub async fn set_conversation(&self, conversation: Conversation) { - *self.conversation.lock().await = Some(conversation); - } } fn tracking_enabled() -> bool { @@ -320,10 +307,7 @@ mod tests { #[tokio::test] async fn test_tracker() { - if let Err(e) = TRACKER - .dispatch(EventKind::Prompt("ping".to_string())) - .await - { + if let Err(e) = TRACKER.dispatch(EventKind::Error("ping".to_string())).await { panic!("Tracker dispatch error: {e:?}"); } } diff --git a/crates/forge_tracker/src/event.rs b/crates/forge_tracker/src/event.rs index ae078aba0d..300965d20c 100644 --- a/crates/forge_tracker/src/event.rs +++ b/crates/forge_tracker/src/event.rs @@ -3,7 +3,6 @@ use std::ops::Deref; use bstr::ByteSlice; use chrono::{DateTime, Utc}; use convert_case::{Case, Casing}; -use forge_domain::Conversation; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -22,7 +21,6 @@ pub struct Event { pub version: String, pub email: Vec, pub model: Option, - pub conversation: Option, pub identity: Option, } @@ -52,24 +50,6 @@ impl From for String { } } -#[derive(Debug, Clone, Serialize)] -pub struct ToolCallPayload { - tool_name: String, - #[serde(skip_serializing_if = "Option::is_none")] - cause: Option, -} - -impl ToolCallPayload { - pub fn new(tool_name: String) -> Self { - Self { tool_name, cause: None } - } - - pub fn with_cause(mut self, cause: String) -> Self { - self.cause = Some(cause); - self - } -} - /// Maximum size (in bytes) of a trace payload sent to collectors. /// Traces beyond this size are truncated to keep tracking minimal. const MAX_TRACE_LEN: usize = 1024; @@ -77,8 +57,6 @@ const MAX_TRACE_LEN: usize = 1024; #[derive(Debug, Clone)] pub enum EventKind { Start, - ToolCall(ToolCallPayload), - Prompt(String), Error(String), Trace(Vec), Login(Identity), @@ -88,9 +66,7 @@ impl EventKind { pub fn name(&self) -> Name { match self { Self::Start => Name::from("start".to_string()), - Self::Prompt(_) => Name::from("prompt".to_string()), Self::Error(_) => Name::from("error".to_string()), - Self::ToolCall(_) => Name::from("tool_call".to_string()), Self::Trace(_) => Name::from("trace".to_string()), Self::Login(_) => Name::from("login".to_string()), } @@ -98,9 +74,7 @@ impl EventKind { pub fn value(&self) -> String { match self { Self::Start => "".to_string(), - Self::Prompt(content) => content.to_string(), Self::Error(content) => content.to_string(), - Self::ToolCall(payload) => serde_json::to_string(&payload).unwrap_or_default(), Self::Trace(trace) => { let text = trace.to_str_lossy(); let mut end = text.len().min(MAX_TRACE_LEN); diff --git a/crates/forge_tracker/src/lib.rs b/crates/forge_tracker/src/lib.rs index e78d2bef67..f5bf3fbaca 100644 --- a/crates/forge_tracker/src/lib.rs +++ b/crates/forge_tracker/src/lib.rs @@ -9,5 +9,5 @@ mod rate_limit; pub use can_track::VERSION; pub use dispatch::Tracker; use error::Result; -pub use event::{Event, EventKind, ToolCallPayload}; +pub use event::{Event, EventKind}; pub use log::{Guard, init_tracing}; From f1d08f96879f95b79df0c7c2a76c1a4a031ee864 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Mon, 10 Aug 2026 11:21:22 +0530 Subject: [PATCH 3/7] chore: drop trace payload truncation limit Traces are now error-level only, so send them in full. --- crates/forge_tracker/src/event.rs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/crates/forge_tracker/src/event.rs b/crates/forge_tracker/src/event.rs index 300965d20c..f587dc2633 100644 --- a/crates/forge_tracker/src/event.rs +++ b/crates/forge_tracker/src/event.rs @@ -50,10 +50,6 @@ impl From for String { } } -/// Maximum size (in bytes) of a trace payload sent to collectors. -/// Traces beyond this size are truncated to keep tracking minimal. -const MAX_TRACE_LEN: usize = 1024; - #[derive(Debug, Clone)] pub enum EventKind { Start, @@ -75,14 +71,7 @@ impl EventKind { match self { Self::Start => "".to_string(), Self::Error(content) => content.to_string(), - Self::Trace(trace) => { - let text = trace.to_str_lossy(); - let mut end = text.len().min(MAX_TRACE_LEN); - while !text.is_char_boundary(end) { - end -= 1; - } - text[..end].to_string() - } + Self::Trace(trace) => trace.to_str_lossy().to_string(), Self::Login(id) => id.login.to_owned(), } } From ff8dae744c071002c9866d862debe52fc719c0bb Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Mon, 10 Aug 2026 11:34:28 +0530 Subject: [PATCH 4/7] chore: unify error tracking via PosthogErrorLayer; drop Trace events - Remove EventKind::Trace and the log-shipping PostHogWriter - Logs always go to local rolling forge.log (info when tracking, debug otherwise) - Add PosthogErrorLayer: error-level tracing events from forge_ modules are dispatched to PostHog as Error events (single pipeline, no double-send) - Remove manual tracker::error()/error_string() helpers; keep error_blocking for the panic hook - Lower event rate limit from 1000/min to 60/min --- crates/forge_main/src/tracker.rs | 18 +------ crates/forge_main/src/ui.rs | 2 - crates/forge_tracker/src/dispatch.rs | 2 +- crates/forge_tracker/src/event.rs | 4 -- crates/forge_tracker/src/log.rs | 79 +++++++++++++++++++--------- 5 files changed, 56 insertions(+), 49 deletions(-) diff --git a/crates/forge_main/src/tracker.rs b/crates/forge_main/src/tracker.rs index 0709d4255d..ef1a060844 100644 --- a/crates/forge_main/src/tracker.rs +++ b/crates/forge_main/src/tracker.rs @@ -2,12 +2,6 @@ use forge_tracker::EventKind; use crate::TRACKER; -/// Helper functions to eliminate duplication of tokio::spawn + TRACKER patterns -/// Generic dispatcher for any event -fn dispatch(event: EventKind) { - tokio::spawn(TRACKER.dispatch(event)); -} - /// Dispatches an event blockingly /// This is useful for events that are not expected to be dispatched in the /// background @@ -18,20 +12,12 @@ fn dispatch_blocking(event: EventKind) { .ok(); } -/// For error events with Debug formatting -pub fn error(error: E) { - dispatch(EventKind::Error(format!("{error:?}"))); -} - +/// For error events with Debug formatting (used by the panic hook, where the +/// tracing pipeline may no longer be available) pub fn error_blocking(error: E) { dispatch_blocking(EventKind::Error(format!("{error:?}"))); } -/// For error events with string input -pub fn error_string(error: String) { - dispatch(EventKind::Error(error)); -} - /// For model setting pub fn set_model(model: String) { tokio::spawn(TRACKER.set_model(model)); diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index d9b4f3e83e..40500d4d44 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -406,7 +406,6 @@ impl A + Send + Sync> UI match result { Ok(exit) => if exit {return Ok(())}, Err(error) => { - tracker::error(&error); tracing::error!(error = ?error); self.spinner.stop(None)?; self.writeln_to_stderr(TitleFormat::error(format!("{error:?}")).display().to_string())?; @@ -418,7 +417,6 @@ impl A + Send + Sync> UI self.spinner.stop(None)?; } Err(error) => { - tracker::error(&error); tracing::error!(error = ?error); self.spinner.stop(None)?; diff --git a/crates/forge_tracker/src/dispatch.rs b/crates/forge_tracker/src/dispatch.rs index 5c40fcafd0..210175cf2f 100644 --- a/crates/forge_tracker/src/dispatch.rs +++ b/crates/forge_tracker/src/dispatch.rs @@ -55,7 +55,7 @@ static CACHED_ARGS: LazyLock> = LazyLock::new(|| std::env::args().sk /// This acts as a rate limiter to prevent runaway loops (e.g. when /// stdout/stderr is closed and every write error triggers another error event) /// while allowing normal tracking to continue for long-running sessions. -const MAX_EVENTS_PER_MINUTE: usize = 1_000; +const MAX_EVENTS_PER_MINUTE: usize = 60; #[derive(Clone)] pub struct Tracker { diff --git a/crates/forge_tracker/src/event.rs b/crates/forge_tracker/src/event.rs index f587dc2633..8745c259a8 100644 --- a/crates/forge_tracker/src/event.rs +++ b/crates/forge_tracker/src/event.rs @@ -1,6 +1,5 @@ use std::ops::Deref; -use bstr::ByteSlice; use chrono::{DateTime, Utc}; use convert_case::{Case, Casing}; use serde::{Deserialize, Serialize}; @@ -54,7 +53,6 @@ impl From for String { pub enum EventKind { Start, Error(String), - Trace(Vec), Login(Identity), } @@ -63,7 +61,6 @@ impl EventKind { match self { Self::Start => Name::from("start".to_string()), Self::Error(_) => Name::from("error".to_string()), - Self::Trace(_) => Name::from("trace".to_string()), Self::Login(_) => Name::from("login".to_string()), } } @@ -71,7 +68,6 @@ impl EventKind { match self { Self::Start => "".to_string(), Self::Error(content) => content.to_string(), - Self::Trace(trace) => trace.to_str_lossy().to_string(), Self::Login(id) => id.login.to_owned(), } } diff --git a/crates/forge_tracker/src/log.rs b/crates/forge_tracker/src/log.rs index ad4de2fe24..79afbbbefd 100644 --- a/crates/forge_tracker/src/log.rs +++ b/crates/forge_tracker/src/log.rs @@ -11,9 +11,7 @@ use crate::can_track::can_track; pub fn init_tracing(log_path: PathBuf, tracker: Tracker) -> anyhow::Result { debug!(path = %log_path.display(), "Initializing logging system in JSON format"); - // If tracking is enabled, use PostHog for logging; otherwise, use a rolling - // file appender. - let (writer, guard, level) = prepare_writer(log_path, tracker); + let (writer, guard, level) = prepare_writer(log_path); // Create a filter that only allows logs from forge_ modules let filter = filter::filter_fn(|metadata| metadata.target().starts_with("forge_")); @@ -31,45 +29,44 @@ pub fn init_tracing(log_path: PathBuf, tracker: Tracker) -> anyhow::Result ( non_blocking::NonBlocking, WorkerGuard, tracing_subscriber::EnvFilter, ) { - let ((non_blocking, guard), env) = if can_track() { - let append = PostHogWriter::new(tracker); - ( - tracing_appender::non_blocking(append), - // Only ship error-level logs to PostHog to keep tracking minimal - tracing_subscriber::EnvFilter::new("forge=error"), - ) + let append = tracing_appender::rolling::daily(log_path, "forge.log"); + let (non_blocking, guard) = tracing_appender::non_blocking(append); + let env = if can_track() { + tracing_subscriber::EnvFilter::new("forge=info") } else { - let append = tracing_appender::rolling::daily(log_path, "forge.log"); - ( - tracing_appender::non_blocking(append), - tracing_subscriber::EnvFilter::new("forge=debug"), - ) + tracing_subscriber::EnvFilter::new("forge=debug") }; (non_blocking, guard, env) } pub struct Guard(#[allow(dead_code)] WorkerGuard); -struct PostHogWriter { +/// A tracing Layer that forwards error-level events from forge_ modules to the +/// tracker as EventKind::Error. This is the single pipeline through which +/// errors reach PostHog; lower-level crates just use `tracing::error!`. +struct PosthogErrorLayer { tracker: Tracker, runtime: tokio::runtime::Runtime, } -impl PostHogWriter { - pub fn new(tracker: Tracker) -> Self { +impl PosthogErrorLayer { + fn new(tracker: Tracker) -> Self { let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .worker_threads(1) @@ -79,17 +76,47 @@ impl PostHogWriter { } } -impl std::io::Write for PostHogWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { +impl Layer for PosthogErrorLayer { + fn on_event( + &self, + event: &tracing::Event<'_>, + _ctx: tracing_subscriber::layer::Context<'_, S>, + ) { + let metadata = event.metadata(); + if *metadata.level() != tracing::Level::ERROR + || !metadata.target().starts_with("forge_") + { + return; + } + + // Render the event's fields (message + structured fields) into a + // single string. + let mut visitor = MessageVisitor::default(); + event.record(&mut visitor); + let mut message = visitor.0; + if let (Some(file), Some(line)) = (metadata.file(), metadata.line()) { + message = format!("{file}:{line} {message}"); + } + let tracker = self.tracker.clone(); - let event_kind = crate::EventKind::Trace(buf.to_vec()); self.runtime.spawn(async move { - let _ = tracker.dispatch(event_kind).await; + let _ = tracker.dispatch(crate::EventKind::Error(message)).await; }); - Ok(buf.len()) } +} + +#[derive(Default)] +struct MessageVisitor(String); - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) +impl tracing::field::Visit for MessageVisitor { + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { + if !self.0.is_empty() { + self.0.push(' '); + } + if field.name() == "message" { + self.0.push_str(&format!("{value:?}")); + } else { + self.0.push_str(&format!("{}={value:?}", field.name())); + } } } From 98a993c2bfbfbd500618b377d7784176c772c0fc Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Mon, 10 Aug 2026 11:47:26 +0530 Subject: [PATCH 5/7] chore: allow clippy::double_must_use at workspace level Newer clippy nightlies flag #[async_trait]-generated methods (which return an already-must-use Pin> with an implicit #[must_use]) as double_must_use, breaking the Lint Fix CI job on all branches. Allow the lint workspace-wide via [workspace.lints] and opt every crate in. --- Cargo.toml | 5 +++++ crates/forge_api/Cargo.toml | 3 +++ crates/forge_app/Cargo.toml | 3 +++ crates/forge_ci/Cargo.toml | 3 +++ crates/forge_config/Cargo.toml | 3 +++ crates/forge_display/Cargo.toml | 3 +++ crates/forge_domain/Cargo.toml | 3 +++ crates/forge_embed/Cargo.toml | 3 +++ crates/forge_eventsource/Cargo.toml | 3 +++ crates/forge_eventsource_stream/Cargo.toml | 3 +++ crates/forge_fs/Cargo.toml | 3 +++ crates/forge_infra/Cargo.toml | 3 +++ crates/forge_json_repair/Cargo.toml | 4 +++- crates/forge_main/Cargo.toml | 3 +++ crates/forge_markdown_stream/Cargo.toml | 3 +++ crates/forge_repo/Cargo.toml | 3 +++ crates/forge_select/Cargo.toml | 3 +++ crates/forge_services/Cargo.toml | 3 +++ crates/forge_snaps/Cargo.toml | 4 +++- crates/forge_spinner/Cargo.toml | 3 +++ crates/forge_stream/Cargo.toml | 4 +++- crates/forge_template/Cargo.toml | 3 +++ crates/forge_test_kit/Cargo.toml | 3 +++ crates/forge_tool_macros/Cargo.toml | 4 +++- crates/forge_tracker/Cargo.toml | 3 +++ crates/forge_walker/Cargo.toml | 4 +++- 26 files changed, 80 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cc5bb9b683..77bd378e41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,11 @@ version = "0.1.0" rust-version = "1.94" edition = "2024" +[workspace.lints.clippy] +# #[async_trait] generates methods returning Pin> with an +# implicit #[must_use]; newer clippy nightlies flag this as double_must_use. +double_must_use = "allow" + [profile.release] lto = true codegen-units = 1 diff --git a/crates/forge_api/Cargo.toml b/crates/forge_api/Cargo.toml index 9a567acfe6..566866dd44 100644 --- a/crates/forge_api/Cargo.toml +++ b/crates/forge_api/Cargo.toml @@ -31,3 +31,6 @@ forge_config.workspace = true tokio = { workspace = true } + +[lints] +workspace = true diff --git a/crates/forge_app/Cargo.toml b/crates/forge_app/Cargo.toml index 6fbd6df6d9..1189e4b847 100644 --- a/crates/forge_app/Cargo.toml +++ b/crates/forge_app/Cargo.toml @@ -58,3 +58,6 @@ insta.workspace = true tokio-stream.workspace = true fake = { version = "5.1.0", features = ["derive"] } forge_domain = { path = "../forge_domain" } + +[lints] +workspace = true diff --git a/crates/forge_ci/Cargo.toml b/crates/forge_ci/Cargo.toml index 03bd2eb0eb..73e6569fa1 100644 --- a/crates/forge_ci/Cargo.toml +++ b/crates/forge_ci/Cargo.toml @@ -13,3 +13,6 @@ derive_setters.workspace = true [dev-dependencies] + +[lints] +workspace = true diff --git a/crates/forge_config/Cargo.toml b/crates/forge_config/Cargo.toml index b7a1822b27..bf14aaaa61 100644 --- a/crates/forge_config/Cargo.toml +++ b/crates/forge_config/Cargo.toml @@ -26,3 +26,6 @@ is_ci.workspace = true pretty_assertions.workspace = true serde_json.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } + +[lints] +workspace = true diff --git a/crates/forge_display/Cargo.toml b/crates/forge_display/Cargo.toml index 2a11aca5fa..2d20b11dec 100644 --- a/crates/forge_display/Cargo.toml +++ b/crates/forge_display/Cargo.toml @@ -20,3 +20,6 @@ two-face = "0.5.1" insta.workspace = true pretty_assertions.workspace = true strip-ansi-escapes.workspace = true + +[lints] +workspace = true diff --git a/crates/forge_domain/Cargo.toml b/crates/forge_domain/Cargo.toml index 966e2af9f6..1d1dfa9030 100644 --- a/crates/forge_domain/Cargo.toml +++ b/crates/forge_domain/Cargo.toml @@ -44,3 +44,6 @@ insta = { workspace = true, features = ["yaml"] } pretty_assertions.workspace = true is_ci.workspace = true fake = { version = "5.1.0", features = ["derive"] } + +[lints] +workspace = true diff --git a/crates/forge_embed/Cargo.toml b/crates/forge_embed/Cargo.toml index c221cc13d4..6b2056a058 100644 --- a/crates/forge_embed/Cargo.toml +++ b/crates/forge_embed/Cargo.toml @@ -8,3 +8,6 @@ rust-version.workspace = true include_dir.workspace = true handlebars.workspace = true anyhow.workspace = true + +[lints] +workspace = true diff --git a/crates/forge_eventsource/Cargo.toml b/crates/forge_eventsource/Cargo.toml index 4f1aad9d8f..e55f5e0672 100644 --- a/crates/forge_eventsource/Cargo.toml +++ b/crates/forge_eventsource/Cargo.toml @@ -20,3 +20,6 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread"] } futures-retry = "0.6" pin-utils = "0.1" rocket = "0.5.0" + +[lints] +workspace = true diff --git a/crates/forge_eventsource_stream/Cargo.toml b/crates/forge_eventsource_stream/Cargo.toml index 781dbd09f0..2c4d18dd50 100644 --- a/crates/forge_eventsource_stream/Cargo.toml +++ b/crates/forge_eventsource_stream/Cargo.toml @@ -19,3 +19,6 @@ http = "1.0" reqwest = { version = "0.11", features = ["stream"] } tokio = { version = "1.0", features = ["macros", "rt"] } url = "2.2" + +[lints] +workspace = true diff --git a/crates/forge_fs/Cargo.toml b/crates/forge_fs/Cargo.toml index 4232e439cc..eea2a60ff9 100644 --- a/crates/forge_fs/Cargo.toml +++ b/crates/forge_fs/Cargo.toml @@ -18,3 +18,6 @@ forge_domain.workspace = true [dev-dependencies] tempfile = "3.27.0" pretty_assertions = "1.4.0" + +[lints] +workspace = true diff --git a/crates/forge_infra/Cargo.toml b/crates/forge_infra/Cargo.toml index 88fed236bb..5ae3270208 100644 --- a/crates/forge_infra/Cargo.toml +++ b/crates/forge_infra/Cargo.toml @@ -58,3 +58,6 @@ serial_test = "4.0" fake = { version = "5.1.0", features = ["derive"] } pretty_assertions.workspace = true forge_domain = { path = "../forge_domain" } + +[lints] +workspace = true diff --git a/crates/forge_json_repair/Cargo.toml b/crates/forge_json_repair/Cargo.toml index b60e51292e..b9f4b5212f 100644 --- a/crates/forge_json_repair/Cargo.toml +++ b/crates/forge_json_repair/Cargo.toml @@ -13,4 +13,6 @@ schemars = { workspace = true } serde_json5 = "0.2.1" [dev-dependencies] -pretty_assertions = { workspace = true } \ No newline at end of file +pretty_assertions = { workspace = true } +[lints] +workspace = true diff --git a/crates/forge_main/Cargo.toml b/crates/forge_main/Cargo.toml index a27d7a3bd7..8552286e66 100644 --- a/crates/forge_main/Cargo.toml +++ b/crates/forge_main/Cargo.toml @@ -88,3 +88,6 @@ pretty_assertions.workspace = true serial_test = "4.0" fake = { version = "5.1.0", features = ["derive"] } forge_domain = { path = "../forge_domain" } + +[lints] +workspace = true diff --git a/crates/forge_markdown_stream/Cargo.toml b/crates/forge_markdown_stream/Cargo.toml index 5449822e33..18653fc656 100644 --- a/crates/forge_markdown_stream/Cargo.toml +++ b/crates/forge_markdown_stream/Cargo.toml @@ -23,3 +23,6 @@ terminal-colorsaurus = "1.0.3" insta.workspace = true strip-ansi-escapes.workspace = true pretty_assertions.workspace = true + +[lints] +workspace = true diff --git a/crates/forge_repo/Cargo.toml b/crates/forge_repo/Cargo.toml index 964f92a021..137aa1d6f4 100644 --- a/crates/forge_repo/Cargo.toml +++ b/crates/forge_repo/Cargo.toml @@ -73,3 +73,6 @@ fake = { version = "5.1.0", features = ["derive"] } derive_setters.workspace = true mockito = { workspace = true } regex = { workspace = true } + +[lints] +workspace = true diff --git a/crates/forge_select/Cargo.toml b/crates/forge_select/Cargo.toml index 1494f9b142..1a5a51cfc0 100644 --- a/crates/forge_select/Cargo.toml +++ b/crates/forge_select/Cargo.toml @@ -18,3 +18,6 @@ tracing.workspace = true [dev-dependencies] pretty_assertions.workspace = true + +[lints] +workspace = true diff --git a/crates/forge_services/Cargo.toml b/crates/forge_services/Cargo.toml index 5e3be29337..d0ba05c5b9 100644 --- a/crates/forge_services/Cargo.toml +++ b/crates/forge_services/Cargo.toml @@ -63,3 +63,6 @@ fake = { version = "5.1.0", features = ["derive"] } forge_test_kit.workspace = true + +[lints] +workspace = true diff --git a/crates/forge_snaps/Cargo.toml b/crates/forge_snaps/Cargo.toml index 4997a3685b..951f16737c 100644 --- a/crates/forge_snaps/Cargo.toml +++ b/crates/forge_snaps/Cargo.toml @@ -17,4 +17,6 @@ forge_domain.workspace = true [dev-dependencies] tokio = { workspace = true, features = ["macros", "rt", "time", "test-util"] } -tempfile.workspace = true \ No newline at end of file +tempfile.workspace = true +[lints] +workspace = true diff --git a/crates/forge_spinner/Cargo.toml b/crates/forge_spinner/Cargo.toml index 2076ff0429..c0021dc47a 100644 --- a/crates/forge_spinner/Cargo.toml +++ b/crates/forge_spinner/Cargo.toml @@ -18,3 +18,6 @@ rand = "0.10.0" [dev-dependencies] pretty_assertions.workspace = true tokio = { workspace = true, features = ["test-util"] } + +[lints] +workspace = true diff --git a/crates/forge_stream/Cargo.toml b/crates/forge_stream/Cargo.toml index e601537459..363a14b1f9 100644 --- a/crates/forge_stream/Cargo.toml +++ b/crates/forge_stream/Cargo.toml @@ -9,4 +9,6 @@ futures.workspace = true tokio.workspace = true [dev-dependencies] -tokio = { workspace = true, features = ["macros", "rt", "time", "test-util"] } \ No newline at end of file +tokio = { workspace = true, features = ["macros", "rt", "time", "test-util"] } +[lints] +workspace = true diff --git a/crates/forge_template/Cargo.toml b/crates/forge_template/Cargo.toml index 8123d00d9a..9ee8a26dbc 100644 --- a/crates/forge_template/Cargo.toml +++ b/crates/forge_template/Cargo.toml @@ -10,3 +10,6 @@ html-escape = "0.2.13" [dev-dependencies] pretty_assertions.workspace = true + +[lints] +workspace = true diff --git a/crates/forge_test_kit/Cargo.toml b/crates/forge_test_kit/Cargo.toml index f169443335..aad5166568 100644 --- a/crates/forge_test_kit/Cargo.toml +++ b/crates/forge_test_kit/Cargo.toml @@ -15,3 +15,6 @@ json = ["serde", "serde_json"] [lib] doctest = false + +[lints] +workspace = true diff --git a/crates/forge_tool_macros/Cargo.toml b/crates/forge_tool_macros/Cargo.toml index 93e102ed34..1e27e2d6d4 100644 --- a/crates/forge_tool_macros/Cargo.toml +++ b/crates/forge_tool_macros/Cargo.toml @@ -10,4 +10,6 @@ proc-macro = true [dependencies] syn.workspace = true quote.workspace = true -proc-macro2.workspace = true \ No newline at end of file +proc-macro2.workspace = true +[lints] +workspace = true diff --git a/crates/forge_tracker/Cargo.toml b/crates/forge_tracker/Cargo.toml index e6d10b80a4..709523d135 100644 --- a/crates/forge_tracker/Cargo.toml +++ b/crates/forge_tracker/Cargo.toml @@ -39,3 +39,6 @@ uuid.workspace = true tokio = { workspace = true, features = ["macros", "rt", "time", "test-util"] } lazy_static.workspace = true pretty_assertions.workspace = true + +[lints] +workspace = true diff --git a/crates/forge_walker/Cargo.toml b/crates/forge_walker/Cargo.toml index 2aed0d7af0..cd55c4fce2 100644 --- a/crates/forge_walker/Cargo.toml +++ b/crates/forge_walker/Cargo.toml @@ -12,4 +12,6 @@ derive_setters.workspace = true [dev-dependencies] pretty_assertions.workspace = true -tempfile.workspace = true \ No newline at end of file +tempfile.workspace = true +[lints] +workspace = true From 5edc6836fecd2ac904f7e4ea9460006a451f306b Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 06:21:10 +0000 Subject: [PATCH 6/7] [autofix.ci] apply automated fixes --- crates/forge_tracker/src/dispatch.rs | 1 - crates/forge_tracker/src/log.rs | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/forge_tracker/src/dispatch.rs b/crates/forge_tracker/src/dispatch.rs index 210175cf2f..a5dc4d1a21 100644 --- a/crates/forge_tracker/src/dispatch.rs +++ b/crates/forge_tracker/src/dispatch.rs @@ -148,7 +148,6 @@ impl Tracker { } guard.clone().unwrap_or_default() } - } fn tracking_enabled() -> bool { diff --git a/crates/forge_tracker/src/log.rs b/crates/forge_tracker/src/log.rs index 79afbbbefd..f8e74ccea5 100644 --- a/crates/forge_tracker/src/log.rs +++ b/crates/forge_tracker/src/log.rs @@ -83,9 +83,7 @@ impl Layer for PosthogErrorLayer { _ctx: tracing_subscriber::layer::Context<'_, S>, ) { let metadata = event.metadata(); - if *metadata.level() != tracing::Level::ERROR - || !metadata.target().starts_with("forge_") - { + if *metadata.level() != tracing::Level::ERROR || !metadata.target().starts_with("forge_") { return; } From 7d4055189555a91aeb1f67210b32eecaf7754f21 Mon Sep 17 00:00:00 2001 From: laststylebender14 Date: Mon, 10 Aug 2026 12:42:28 +0530 Subject: [PATCH 7/7] test: cover PosthogErrorLayer dispatch and filtering - Add Tracker::with_collectors test constructor for collector injection - Add capture_events fixture that runs a closure under the layer and returns all dispatched (name, value) pairs - Verify: error events dispatch with exact file:line/message/field rendering, non-error levels and non-forge targets are ignored, and multiple errors arrive as distinct ordered events --- crates/forge_tracker/src/dispatch.rs | 15 +++++ crates/forge_tracker/src/log.rs | 96 ++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/crates/forge_tracker/src/dispatch.rs b/crates/forge_tracker/src/dispatch.rs index a5dc4d1a21..a087c5cc94 100644 --- a/crates/forge_tracker/src/dispatch.rs +++ b/crates/forge_tracker/src/dispatch.rs @@ -86,6 +86,21 @@ impl Default for Tracker { } impl Tracker { + /// Creates a tracker with custom collectors; tracking is always enabled. + /// Intended for tests. + #[cfg(test)] + pub(crate) fn with_collectors(collectors: Vec>) -> Self { + Self { + collectors: Arc::new(collectors), + can_track: true, + start_time: Utc::now(), + email: Arc::new(Mutex::new(Some(vec![]))), + model: Arc::new(Mutex::new(None)), + is_logged_in: Arc::new(AtomicBool::new(false)), + rate_limiter: Arc::new(Mutex::new(RateLimiter::new(MAX_EVENTS_PER_MINUTE))), + } + } + pub async fn set_model>(&'static self, model: S) { let mut guard = self.model.lock().await; *guard = Some(model.into()); diff --git a/crates/forge_tracker/src/log.rs b/crates/forge_tracker/src/log.rs index f8e74ccea5..5fd0ca744f 100644 --- a/crates/forge_tracker/src/log.rs +++ b/crates/forge_tracker/src/log.rs @@ -118,3 +118,99 @@ impl tracing::field::Visit for MessageVisitor { } } } + +#[cfg(test)] +mod tests { + use std::sync::mpsc; + use std::time::Duration; + + use pretty_assertions::assert_eq; + use tracing_subscriber::prelude::*; + + use super::*; + use crate::collect::Collect; + use crate::{Event, Tracker}; + + struct ChannelCollector(std::sync::Mutex>); + + #[async_trait::async_trait] + impl Collect for ChannelCollector { + async fn collect(&self, event: Event) -> crate::Result<()> { + self.0.lock().unwrap().send(event).ok(); + Ok(()) + } + } + + /// Runs the given closure with a PosthogErrorLayer installed and returns + /// the `(name, value)` of every event it dispatched. + fn capture_events(f: impl FnOnce()) -> Vec<(String, String)> { + let (tx, rx) = mpsc::channel(); + let tracker = + Tracker::with_collectors(vec![Box::new(ChannelCollector(std::sync::Mutex::new(tx)))]); + let subscriber = tracing_subscriber::registry().with(PosthogErrorLayer::new(tracker)); + + // Keep the subscriber (and the layer's runtime) alive until all + // dispatched events have been drained. + let guard = tracing::subscriber::set_default(subscriber); + f(); + + let mut events = vec![]; + while let Ok(event) = rx.recv_timeout(Duration::from_millis(200)) { + events.push((event.event_name.to_string(), event.event_value)); + } + drop(guard); + events + } + + #[test] + fn test_error_event_is_dispatched_with_message_and_fields() { + let mut line = 0; + let actual = capture_events(|| { + tracing::error!(target: "forge_test", code = 42, "something failed"); + line = line!() - 1; // line of the error! call above + }); + + let expected = vec![( + "error".to_string(), + format!("{}:{line} something failed code=42", file!()), + )]; + assert_eq!(actual, expected); + } + + #[test] + fn test_non_error_levels_are_ignored() { + let actual = capture_events(|| { + tracing::warn!(target: "forge_test", "a warning"); + tracing::info!(target: "forge_test", "an info"); + tracing::debug!(target: "forge_test", "a debug"); + }); + + let expected: Vec<(String, String)> = vec![]; + assert_eq!(actual, expected); + } + + #[test] + fn test_non_forge_targets_are_ignored() { + let actual = capture_events(|| { + tracing::error!(target: "hyper::client", "external error"); + }); + + let expected: Vec<(String, String)> = vec![]; + assert_eq!(actual, expected); + } + + #[test] + fn test_multiple_errors_are_dispatched_in_order() { + let actual: Vec = capture_events(|| { + tracing::error!(target: "forge_test", "first"); + tracing::error!(target: "forge_test", "second"); + }) + .into_iter() + .map(|(_, value)| value) + .collect(); + + assert_eq!(actual.len(), 2); + assert!(actual[0].ends_with("first")); + assert!(actual[1].ends_with("second")); + } +}