Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ session-analytics-cli agents # Task subagent activity vs main sessi
session-analytics-cli signals # Raw session metrics for LLM interpretation
session-analytics-cli classify # Categorize sessions (debug/dev/research)
session-analytics-cli failures # Error patterns and rework detection
session-analytics-cli error-details # Detailed errors with tool parameters
session-analytics-cli trends # Compare usage across time periods
session-analytics-cli handoff # Context summary for session handoff

Expand Down Expand Up @@ -91,7 +92,7 @@ All commands support:

## MCP Tools

30 tools available when running as an MCP server:
31 tools available when running as an MCP server:

| Category | Tools |
|----------|-------|
Expand All @@ -100,7 +101,7 @@ All commands support:
| **Patterns** | `get_tool_sequences`, `sample_sequences`, `get_permission_gaps`, `get_insights` |
| **Files** | `get_file_activity`, `get_languages`, `get_projects`, `get_mcp_usage` |
| **Agents** | `get_agent_activity` |
| **Sessions** | `get_session_signals`, `classify_sessions`, `analyze_failures`, `analyze_trends`, `get_handoff_context` |
| **Sessions** | `get_session_signals`, `classify_sessions`, `analyze_failures`, `get_error_details`, `analyze_trends`, `get_handoff_context` |
| **Messages** | `get_session_messages`, `search_messages` |
| **Relationships** | `detect_parallel_sessions`, `find_related_sessions` |
| **Git** | `ingest_git_history`, `correlate_git_with_sessions`, `get_session_commits` |
Expand Down
58 changes: 58 additions & 0 deletions src/session_analytics/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
query_agent_activity,
query_bus_events,
query_commands,
query_error_details,
query_file_activity,
query_languages,
query_mcp_usage,
Expand Down Expand Up @@ -400,6 +401,44 @@ def _format_failures(data: dict) -> list[str]:
return lines


@_register_formatter(lambda d: "errors_by_tool" in d and "tool_totals" in d)
def _format_error_details(data: dict) -> list[str]:
lines = [
f"Error Details (last {data['days']} days)",
f"Total errors: {data['total_errors']}",
]
if data.get("tool_filter"):
lines.append(f"Filter: {data['tool_filter']}")
lines.append("")

errors_by_tool = data.get("errors_by_tool", {})
tool_totals = data.get("tool_totals", {})

if not errors_by_tool:
lines.append("No errors found.")
return lines

for tool_name in sorted(errors_by_tool.keys(), key=lambda t: -tool_totals.get(t, 0)):
total = tool_totals.get(tool_name, 0)
lines.append(f"{tool_name} ({total} errors):")
for err in errors_by_tool[tool_name][:10]:
param = err.get("param_value") or "(unknown)"
count = err.get("error_count", 0)
suffix = ""
if err.get("search_path"):
suffix = f" in {err['search_path']}"
elif err.get("project"):
# Extract repo name from project path
proj = err["project"]
if proj:
proj = proj.split("-")[-1] if "-" in proj else proj
suffix = f" ({proj})"
lines.append(f" {param!r}: {count} errors{suffix}")
lines.append("")

return lines


@_register_formatter(lambda d: "category_distribution" in d and "sessions" in d)
def _format_classify_sessions(data: dict) -> list[str]:
lines = [
Expand Down Expand Up @@ -806,6 +845,18 @@ def cmd_failures(args):
print(format_output(result, args.json))


def cmd_error_details(args):
"""Show detailed error information with tool parameters."""
storage = SQLiteStorage()
result = query_error_details(
storage,
days=args.days,
tool=args.tool,
limit=args.limit,
)
print(format_output(result, args.json))


def cmd_classify(args):
"""Show session classifications."""
storage = SQLiteStorage()
Expand Down Expand Up @@ -1069,6 +1120,13 @@ def main():
)
sub.set_defaults(func=cmd_failures)

# error-details
sub = subparsers.add_parser("error-details", help="Show error details with tool parameters")
sub.add_argument("--days", type=int, default=7, help="Days to analyze (default: 7)")
sub.add_argument("--tool", help="Filter by tool name (e.g., Glob, Bash, Edit)")
sub.add_argument("--limit", type=int, default=50, help="Max errors per tool (default: 50)")
sub.set_defaults(func=cmd_error_details)

# classify
sub = subparsers.add_parser("classify", help="Classify sessions by activity type")
sub.add_argument("--days", type=int, default=7, help="Days to analyze (default: 7)")
Expand Down
8 changes: 7 additions & 1 deletion src/session_analytics/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,18 @@ identify permission gaps.
| Tool | Purpose |
|------|---------|
| `analyze_failures(days?, project?)` | Failure patterns with drill-down to specific commands |
| `get_error_details(days?, tool?, limit?)` | Detailed errors with tool parameters (patterns, commands, files) |

Returns:
`analyze_failures()` returns:
- `errors_by_tool`: Count of errors per tool
- `error_examples`: Top failing commands (Bash) or files (Edit/Read/Write) for drill-down
- `rework_patterns`: Files edited 3+ times within 10 minutes

`get_error_details()` shows *which specific parameters* caused failures:
- Glob/Grep: The pattern that failed (e.g., `"*"` with 922 errors)
- Bash: The command that failed (e.g., `pwd` with 492 errors)
- Edit/Read/Write: The file path that failed

### Session Classification

| Tool | Purpose |
Expand Down
106 changes: 106 additions & 0 deletions src/session_analytics/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -1899,3 +1899,109 @@ def query_bus_events(
"event_types": type_counts,
"events": events,
}


def query_error_details(
storage: SQLiteStorage,
days: int = 7,
tool: str | None = None,
limit: int = 50,
) -> dict:
"""Get detailed error information including tool parameters that caused failures.

Joins tool_result errors with tool_use events to extract the parameters
(pattern for Glob/Grep, command for Bash, file_path for file operations)
that caused the failure.

Args:
storage: Storage instance
days: Number of days to analyze (default: 7)
tool: Optional filter by tool name (e.g., "Glob", "Bash")
limit: Maximum errors to return per tool (default: 50)

Returns:
Dict with error details grouped by tool and parameter
"""
cutoff = get_cutoff(days=days)

# Build tool filter
tool_filter = ""
params: list = [cutoff]
if tool:
tool_filter = "AND e2.tool_name = ?"
params.append(tool)

# Query errors with tool parameters
# Uses json_extract to get the relevant parameter based on tool type:
# - Glob/Grep: pattern
# - Bash: command (already extracted to column)
# - Read/Edit/Write: file_path (already extracted to column)
rows = storage.execute_query(
f"""
SELECT
e2.tool_name,
e2.command,
e2.file_path,
json_extract(e2.tool_input_json, '$.pattern') as pattern,
json_extract(e2.tool_input_json, '$.path') as search_path,
e1.project_path,
COUNT(*) as error_count
FROM events e1
JOIN events e2 ON e1.tool_id = e2.tool_id AND e2.entry_type = 'tool_use'
WHERE e1.timestamp >= ?
AND e1.is_error = 1
AND e1.entry_type = 'tool_result'
{tool_filter}
GROUP BY e2.tool_name, e2.command, e2.file_path, pattern, search_path, e1.project_path
ORDER BY e2.tool_name, error_count DESC
""",
tuple(params),
)

# Organize by tool with the relevant parameter
errors_by_tool: dict[str, list[dict]] = {}
tool_totals: dict[str, int] = {}

for row in rows:
tool_name = row["tool_name"]
if not tool_name:
continue

# Determine the key parameter based on tool type
if tool_name in ("Glob", "Grep"):
key_param = row["pattern"]
param_type = "pattern"
elif tool_name == "Bash":
key_param = row["command"]
param_type = "command"
else:
key_param = row["file_path"]
param_type = "file_path"

if tool_name not in errors_by_tool:
errors_by_tool[tool_name] = []
tool_totals[tool_name] = 0

tool_totals[tool_name] += row["error_count"]

# Only keep top N per tool
if len(errors_by_tool[tool_name]) < limit:
error_detail = {
"param_type": param_type,
"param_value": key_param,
"error_count": row["error_count"],
"project": row["project_path"],
}
# Add search_path for Glob/Grep if present
if tool_name in ("Glob", "Grep") and row["search_path"]:
error_detail["search_path"] = row["search_path"]

errors_by_tool[tool_name].append(error_detail)

return {
"days": days,
"tool_filter": tool,
"errors_by_tool": errors_by_tool,
"tool_totals": tool_totals,
"total_errors": sum(tool_totals.values()),
}
20 changes: 20 additions & 0 deletions src/session_analytics/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,26 @@ def analyze_failures(days: int = 7, rework_window_minutes: int = 10) -> dict:
return {"status": "ok", **result}


@mcp.tool()
def get_error_details(days: int = 7, tool: str | None = None, limit: int = 50) -> dict:
"""Get detailed error information including tool parameters that caused failures.

Shows which specific patterns (Glob/Grep), commands (Bash), or files caused errors.
Use this to drill down from analyze_failures() counts to actionable specifics.

Args:
days: Number of days to analyze (default: 7)
tool: Optional filter by tool name (e.g., "Glob", "Bash", "Edit")
limit: Maximum errors to return per tool (default: 50)

Returns:
Error details grouped by tool with the failing parameter (pattern/command/file)
"""
queries.ensure_fresh_data(storage, days=days)
result = queries.query_error_details(storage, days=days, tool=tool, limit=limit)
return {"status": "ok", **result}


@mcp.tool()
def classify_sessions(days: int = 7, project: str | None = None) -> dict:
"""Classify sessions based on their dominant activity patterns.
Expand Down
Loading