feat(trace,file-history): add tracing instrumentation and diagnostic hints

Add structured tracing spans to trace and file-history pipelines so debug
logging (-vv) shows path resolution counts, MR match counts, and discussion
counts at each stage. This makes empty-result debugging straightforward.

Add a hints field to TraceResult and FileHistoryResult that carries
machine-readable diagnostic strings explaining *why* results may be empty
(e.g., "Run 'lore sync' to fetch MR file changes"). The CLI renders these
as info lines; robot mode includes them in JSON when non-empty.

Also: fix filter_map(Result::ok) → collect::<Result> in trace.rs (same
pattern fixed in prior commit for file_history/path_resolver), and switch
conn.prepare → conn.prepare_cached for the MR query.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
teernisse
2026-02-18 13:28:37 -05:00
parent c0ca501662
commit 8442bcf367
3 changed files with 192 additions and 27 deletions

View File

@@ -1,5 +1,7 @@
use serde::Serialize;
use tracing::info;
use crate::Config;
use crate::cli::render::{self, Icons, Theme};
use crate::core::db::create_connection;
@@ -46,6 +48,9 @@ pub struct FileHistoryResult {
pub discussions: Vec<FileDiscussion>,
pub total_mrs: usize,
pub paths_searched: usize,
/// Diagnostic hints explaining why results may be empty.
#[serde(skip_serializing_if = "Vec::is_empty")]
pub hints: Vec<String>,
}
/// Run the file-history query.
@@ -77,6 +82,11 @@ pub fn run_file_history(
let paths_searched = all_paths.len();
info!(
paths = paths_searched,
renames_followed, "file-history: resolved {} path(s) for '{}'", paths_searched, path
);
// Build placeholders for IN clause
let placeholders: Vec<String> = (0..all_paths.len())
.map(|i| format!("?{}", i + 2))
@@ -135,14 +145,31 @@ pub fn run_file_history(
web_url: row.get(8)?,
})
})?
.filter_map(std::result::Result::ok)
.collect();
.collect::<std::result::Result<Vec<_>, _>>()?;
let total_mrs = merge_requests.len();
info!(
mr_count = total_mrs,
"file-history: found {} MR(s) touching '{}'", total_mrs, path
);
// Optionally fetch DiffNote discussions on this file
let discussions = if include_discussions && !merge_requests.is_empty() {
fetch_file_discussions(&conn, &all_paths, project_id)?
let discs = fetch_file_discussions(&conn, &all_paths, project_id)?;
info!(
discussion_count = discs.len(),
"file-history: found {} discussion(s)",
discs.len()
);
discs
} else {
Vec::new()
};
// Build diagnostic hints when no results found
let hints = if total_mrs == 0 {
build_file_history_hints(&conn, project_id, &all_paths)?
} else {
Vec::new()
};
@@ -155,6 +182,7 @@ pub fn run_file_history(
discussions,
total_mrs,
paths_searched,
hints,
})
}
@@ -179,8 +207,7 @@ fn fetch_file_discussions(
JOIN discussions d ON d.id = n.discussion_id \
WHERE n.position_new_path IN ({in_clause}) {project_filter} \
AND n.is_system = 0 \
ORDER BY n.created_at DESC \
LIMIT 50"
ORDER BY n.created_at DESC"
);
let mut stmt = conn.prepare(&sql)?;
@@ -210,12 +237,57 @@ fn fetch_file_discussions(
created_at_iso: ms_to_iso(created_at),
})
})?
.filter_map(std::result::Result::ok)
.collect();
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(discussions)
}
/// Build diagnostic hints explaining why a file-history query returned no results.
fn build_file_history_hints(
conn: &rusqlite::Connection,
project_id: Option<i64>,
paths: &[String],
) -> Result<Vec<String>> {
let mut hints = Vec::new();
// Check if mr_file_changes has ANY rows for this project
let has_file_changes: bool = if let Some(pid) = project_id {
conn.query_row(
"SELECT EXISTS(SELECT 1 FROM mr_file_changes WHERE project_id = ?1 LIMIT 1)",
rusqlite::params![pid],
|row| row.get(0),
)?
} else {
conn.query_row(
"SELECT EXISTS(SELECT 1 FROM mr_file_changes LIMIT 1)",
[],
|row| row.get(0),
)?
};
if !has_file_changes {
hints.push(
"No MR file changes have been synced yet. Run 'lore sync' to fetch file change data."
.to_string(),
);
return Ok(hints);
}
// File changes exist but none match these paths
let path_list = paths
.iter()
.map(|p| format!("'{p}'"))
.collect::<Vec<_>>()
.join(", ");
hints.push(format!(
"Searched paths [{}] were not found in MR file changes. \
The file may predate the sync window or use a different path.",
path_list
));
Ok(hints)
}
// ── Human output ────────────────────────────────────────────────────────────
pub fn print_file_history(result: &FileHistoryResult) {
@@ -250,10 +322,16 @@ pub fn print_file_history(result: &FileHistoryResult) {
Icons::info(),
Theme::dim().render("No merge requests found touching this file.")
);
println!(
" {}",
Theme::dim().render("Hint: Run 'lore sync' to fetch MR file changes.")
);
if !result.renames_followed && result.rename_chain.len() == 1 {
println!(
" {} Searched: {}",
Icons::info(),
Theme::dim().render(&result.rename_chain[0])
);
}
for hint in &result.hints {
println!(" {} {}", Icons::info(), Theme::dim().render(hint));
}
println!();
return;
}
@@ -327,6 +405,7 @@ pub fn print_file_history_json(result: &FileHistoryResult, elapsed_ms: u64) {
"total_mrs": result.total_mrs,
"renames_followed": result.renames_followed,
"paths_searched": result.paths_searched,
"hints": if result.hints.is_empty() { None } else { Some(&result.hints) },
}
});