Skip to content

Commit 9401ace

Browse files
amrit110claude
andcommitted
Add BookStack analytics dashboard with user and page title tracking
- Add GCS-backed activity logger (BookstackActivityLogger) that records per-query traces and a unified activity log to GCS - Add analytics page at /aieng-bot/analytics with metrics cards, query velocity chart, tool usage chart, and recent queries table - Log authenticated user email per query (injected by Next.js proxy) - Enrich get_page tool call traces with resolved page title from tool_resolve events so the trace viewer shows titles instead of IDs - Add User column to recent queries table (visible on lg+ screens) - Stack velocity and tool usage charts vertically at full width Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4075965 commit 9401ace

File tree

7 files changed

+90
-32
lines changed

7 files changed

+90
-32
lines changed

bookstack_agent/api/main.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ async def _log_query_bg(
179179
answer: str,
180180
duration_seconds: float,
181181
status: str,
182+
user_email: str | None,
182183
) -> None:
183184
"""Run analytics logging in a thread pool (non-blocking)."""
184185
try:
@@ -190,6 +191,7 @@ async def _log_query_bg(
190191
answer,
191192
duration_seconds,
192193
status,
194+
user_email,
193195
)
194196
except Exception as exc: # noqa: BLE001
195197
api_logger.warning("Analytics logging failed (non-fatal): %s", exc)
@@ -209,6 +211,11 @@ class AskRequest(BaseModel):
209211
description="Opaque session token returned by a previous response. "
210212
"Omit to start a new conversation.",
211213
)
214+
user_email: str | None = Field(
215+
default=None,
216+
description="Email of the authenticated user. Injected by the Next.js "
217+
"proxy from the server-side session; ignored if sent directly.",
218+
)
212219

213220

214221
# ---------------------------------------------------------------------------
@@ -261,10 +268,28 @@ async def event_stream() -> AsyncGenerator[str, None]:
261268

262269
if event_type == "tool_use":
263270
query_tool_calls.append(
264-
{"tool": event.get("tool", ""), "input": event.get("input", {})}
271+
{
272+
"tool": event.get("tool", ""),
273+
"input": dict(event.get("input", {})),
274+
}
265275
)
266276
yield f"data: {json.dumps(event)}\n\n"
267277

278+
elif event_type == "tool_resolve":
279+
# Enrich the matching get_page tool call with the resolved title
280+
page_id = event.get("page_id")
281+
page_title = event.get("page_title")
282+
if page_id is not None and page_title:
283+
for tc in reversed(query_tool_calls):
284+
if (
285+
tc["tool"] == "get_page"
286+
and tc["input"].get("page_id") == page_id
287+
and "page_title" not in tc["input"]
288+
):
289+
tc["input"]["page_title"] = page_title
290+
break
291+
yield f"data: {json.dumps(event)}\n\n"
292+
268293
elif event_type == "answer":
269294
updated_history = event.pop("history", history)
270295
final_answer = event.get("text", "")
@@ -292,6 +317,7 @@ async def event_stream() -> AsyncGenerator[str, None]:
292317
answer=final_answer,
293318
duration_seconds=duration,
294319
status=final_status,
320+
user_email=request.user_email,
295321
)
296322
)
297323

bookstack_agent/ui/app/analytics/components/recent-queries-table.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { useState } from 'react'
4-
import { Search, ChevronDown, ChevronUp, Clock, CheckCircle, XCircle, Wrench } from 'lucide-react'
4+
import { Search, ChevronDown, ChevronUp, Clock, CheckCircle, XCircle, Wrench, User } from 'lucide-react'
55
import type { BookstackActivity, BookstackTrace } from '@/lib/bookstack-types'
66

77
const TOOL_COLORS: Record<string, string> = {
@@ -64,9 +64,16 @@ function TraceModal({
6464
<div className="flex items-center gap-2 mb-1.5">
6565
<span className="text-xs text-slate-500">#{tc.seq}</span>
6666
<ToolBadge tool={tc.tool} />
67+
{tc.tool === 'get_page' && tc.input.page_title && (
68+
<span className="text-xs text-slate-300 font-medium truncate">
69+
{String(tc.input.page_title)}
70+
</span>
71+
)}
6772
</div>
68-
<pre className="text-xs text-slate-300 whitespace-pre-wrap break-words font-mono leading-relaxed">
69-
{JSON.stringify(tc.input, null, 2)}
73+
<pre className="text-xs text-slate-500 whitespace-pre-wrap break-words font-mono leading-relaxed">
74+
{tc.tool === 'get_page'
75+
? `page_id: ${tc.input.page_id}`
76+
: JSON.stringify(tc.input, null, 2)}
7077
</pre>
7178
</div>
7279
))}
@@ -179,6 +186,7 @@ export default function RecentQueriesTable({ activities }: RecentQueriesTablePro
179186
<tr className="border-b border-white/10">
180187
<th className="pb-3 text-left text-xs font-semibold text-slate-400 uppercase tracking-wider">Question</th>
181188
<th className="pb-3 text-left text-xs font-semibold text-slate-400 uppercase tracking-wider hidden md:table-cell">Tools</th>
189+
<th className="pb-3 text-left text-xs font-semibold text-slate-400 uppercase tracking-wider hidden lg:table-cell">User</th>
182190
<th className="pb-3 text-right text-xs font-semibold text-slate-400 uppercase tracking-wider hidden sm:table-cell">Duration</th>
183191
<th className="pb-3 text-center text-xs font-semibold text-slate-400 uppercase tracking-wider">Status</th>
184192
<th className="pb-3 text-right text-xs font-semibold text-slate-400 uppercase tracking-wider">Timestamp</th>
@@ -209,6 +217,16 @@ export default function RecentQueriesTable({ activities }: RecentQueriesTablePro
209217
)}
210218
</div>
211219
</td>
220+
<td className="py-3 pr-4 hidden lg:table-cell">
221+
{activity.user_email ? (
222+
<span className="flex items-center gap-1 text-xs text-slate-400">
223+
<User className="w-3 h-3 shrink-0" />
224+
<span className="truncate max-w-[140px]">{activity.user_email}</span>
225+
</span>
226+
) : (
227+
<span className="text-xs text-slate-600"></span>
228+
)}
229+
</td>
212230
<td className="py-3 pr-4 text-right hidden sm:table-cell">
213231
<span className="flex items-center justify-end gap-1 text-slate-400 text-xs">
214232
<Clock className="w-3 h-3" />

bookstack_agent/ui/app/analytics/components/tool-usage-chart.tsx

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,19 @@ export default function ToolUsageChart({ metrics }: { metrics: BookstackMetrics
4242

4343
return (
4444
<div className="rounded-xl border border-white/10 bg-slate-800/60 p-6">
45-
<div className="mb-5">
45+
<div className="mb-6">
4646
<h2 className="text-xl font-bold text-white">Tool Usage</h2>
4747
<p className="text-sm text-slate-400 mt-0.5">
4848
{total.toLocaleString()} total calls across all queries
4949
</p>
5050
</div>
5151

52-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
52+
{/* Full-width horizontal layout: bar chart left, breakdown cards right */}
53+
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
5354
{/* Bar chart */}
54-
<div className="h-56">
55+
<div className="h-48">
5556
<ResponsiveContainer width="100%" height="100%">
56-
<BarChart data={data} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
57+
<BarChart data={data} margin={{ top: 4, right: 16, left: 0, bottom: 4 }}>
5758
<CartesianGrid strokeDasharray="3 3" stroke="#334155" opacity={0.4} />
5859
<XAxis
5960
dataKey="label"
@@ -75,6 +76,7 @@ export default function ToolUsageChart({ metrics }: { metrics: BookstackMetrics
7576
color: '#fff',
7677
}}
7778
labelStyle={{ color: '#94a3b8' }}
79+
cursor={{ fill: 'rgba(255,255,255,0.05)' }}
7880
/>
7981
<Bar dataKey="count" name="Calls" radius={[4, 4, 0, 0]}>
8082
{data.map((entry) => (
@@ -85,31 +87,33 @@ export default function ToolUsageChart({ metrics }: { metrics: BookstackMetrics
8587
</ResponsiveContainer>
8688
</div>
8789

88-
{/* Tool breakdown list */}
89-
<div className="flex flex-col justify-center gap-3">
90+
{/* Breakdown cards */}
91+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
9092
{data.map((entry) => {
9193
const pct = total > 0 ? Math.round((entry.count / total) * 100) : 0
9294
return (
93-
<div key={entry.tool}>
94-
<div className="flex items-center justify-between mb-1">
95-
<div className="flex items-center gap-2">
96-
<span
97-
className="inline-block w-2.5 h-2.5 rounded-full"
98-
style={{ backgroundColor: entry.color }}
99-
/>
100-
<span className="text-sm font-medium text-slate-200">{entry.label}</span>
101-
</div>
102-
<span className="text-sm text-slate-400">
103-
{entry.count.toLocaleString()} ({pct}%)
95+
<div
96+
key={entry.tool}
97+
className="rounded-lg border border-white/10 bg-slate-700/40 p-4"
98+
>
99+
<div className="flex items-center gap-2 mb-2">
100+
<span
101+
className="inline-block w-2.5 h-2.5 rounded-full shrink-0"
102+
style={{ backgroundColor: entry.color }}
103+
/>
104+
<span className="text-sm font-semibold text-slate-200 truncate">
105+
{entry.label}
104106
</span>
105107
</div>
106-
<div className="h-1.5 rounded-full bg-slate-700 overflow-hidden">
108+
<p className="text-2xl font-bold text-white">{entry.count.toLocaleString()}</p>
109+
<p className="text-xs text-slate-400 mt-0.5">{pct}% of calls</p>
110+
<div className="mt-2 h-1 rounded-full bg-slate-600 overflow-hidden">
107111
<div
108112
className="h-full rounded-full transition-all duration-500"
109113
style={{ width: `${pct}%`, backgroundColor: entry.color }}
110114
/>
111115
</div>
112-
<p className="text-xs text-slate-500 mt-1">{TOOL_DESC[entry.tool]}</p>
116+
<p className="text-xs text-slate-500 mt-2 leading-snug">{TOOL_DESC[entry.tool]}</p>
113117
</div>
114118
)
115119
})}

bookstack_agent/ui/app/analytics/page.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,11 @@ export default async function BookstackAnalyticsPage() {
9595
{/* Metrics row */}
9696
<QueryMetrics metrics={metrics} />
9797

98-
{/* Velocity + Tool usage side by side on large screens */}
99-
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
100-
<div className="lg:col-span-2">
101-
<QueryVelocityChart data={velocityData} />
102-
</div>
103-
<div>
104-
<ToolUsageChart metrics={metrics} />
105-
</div>
106-
</div>
98+
{/* Velocity chart — full width */}
99+
<QueryVelocityChart data={velocityData} />
100+
101+
{/* Tool usage — full width below velocity */}
102+
<ToolUsageChart metrics={metrics} />
107103

108104
{/* Recent queries */}
109105
<RecentQueriesTable activities={recentActivities} />

bookstack_agent/ui/app/api/ask/route.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,19 @@
55
* Using a server-side proxy keeps BOOKSTACK_API_URL out of the browser and
66
* avoids CORS issues between the UI and the Python backend.
77
*/
8+
import { getCurrentUser } from '@/lib/session'
9+
810
export const dynamic = 'force-dynamic'
911

1012
export async function POST(req: Request): Promise<Response> {
1113
const body = await req.json()
1214

15+
// Inject the authenticated user's email so the backend can log it.
16+
const user = await getCurrentUser()
17+
if (user?.email) {
18+
body.user_email = user.email
19+
}
20+
1321
const backendUrl = process.env.BOOKSTACK_API_URL ?? 'http://localhost:8000'
1422

1523
let upstream: Response

bookstack_agent/ui/lib/bookstack-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export interface BookstackActivity {
88
timestamp: string
99
/** Question text, truncated to 300 chars in the log. */
1010
question: string
11+
/** Email of the authenticated user who asked the question, if available. */
12+
user_email?: string | null
1113
/** Unique tool names used during this query (e.g. ["search_bookstack", "get_page"]). */
1214
tools_used: string[]
1315
/** Total number of individual tool invocations. */

src/aieng_bot/bookstack/activity_logger.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ def log_query(
166166
answer: str,
167167
duration_seconds: float,
168168
status: str,
169+
user_email: str | None = None,
169170
) -> bool:
170171
"""Record a completed BookStack QA query to GCS.
171172
@@ -193,6 +194,8 @@ def log_query(
193194
Wall-clock time from question receipt to answer emission.
194195
status : str
195196
``"success"`` or ``"error"``.
197+
user_email : str or None
198+
Email of the authenticated user who asked the question, if available.
196199
197200
Returns
198201
-------
@@ -244,6 +247,7 @@ def log_query(
244247
"session_id": session_id,
245248
"timestamp": timestamp,
246249
"question": question[:300], # keep activity log compact
250+
"user_email": user_email,
247251
"tools_used": tools_used,
248252
"tool_call_counts": tool_call_counts,
249253
"num_tool_calls": len(tool_calls),

0 commit comments

Comments
 (0)