From 99b542aff47ba5c69e3e32f702b04a38dcd37740 Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 26 Mar 2026 10:30:39 +0100 Subject: [PATCH 1/6] feat: rewrite analytics transcript to chat-bubble style matching session page --- .../session-detail/SessionTranscript.svelte | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/web/src/lib/components/session-detail/SessionTranscript.svelte b/web/src/lib/components/session-detail/SessionTranscript.svelte index 952cc67..8ac33a5 100644 --- a/web/src/lib/components/session-detail/SessionTranscript.svelte +++ b/web/src/lib/components/session-detail/SessionTranscript.svelte @@ -1,6 +1,4 @@ @@ -72,7 +68,9 @@ {@const isActive = activeFilters.size === 0 || activeFilters.has(type)} + {#if showCharts} +
+ +
+ {/if} - {:else} -
- - + +
+ + {#if showTranscript} +
+ +
+ {/if}
- {/if} +
{/if} diff --git a/web/src/routes/orgs/[slug]/traces/sessions/[id]/+page.svelte b/web/src/routes/orgs/[slug]/traces/sessions/[id]/+page.svelte index 74bf17a..c653926 100644 --- a/web/src/routes/orgs/[slug]/traces/sessions/[id]/+page.svelte +++ b/web/src/routes/orgs/[slug]/traces/sessions/[id]/+page.svelte @@ -65,6 +65,7 @@ let expandedEvents = $state(new Set()); let expandedFiles = $state(new Set()); + let transcriptFilters = $state(new Set()); let sectionsOpen = $state({ events: true, files: false, @@ -195,6 +196,18 @@ expandedFiles = next; } + function toggleTranscriptFilter(role: string) { + const next = new Set(transcriptFilters); + if (next.has(role)) next.delete(role); + else next.add(role); + transcriptFilters = next; + } + + const ROLE_COLORS: Record = { + user: '#3ecf8e', + assistant: '#a78bfa' + }; + interface DiffLine { type: 'add' | 'remove' | 'header'; content: string; @@ -551,6 +564,7 @@

No transcript data.

{:else} {@const turns = extractTurns(data.transcript_chunks)} + {@const roleCounts = turns.reduce((acc, t) => { acc[t.role] = (acc[t.role] || 0) + 1; return acc; }, {} as Record)} {#if turns.length === 0}
{#each data.transcript_chunks as chunk (chunk.chunk_index)} @@ -558,8 +572,21 @@ {/each}
{:else} +
+ {#each Object.entries(roleCounts) as [role, count]} + {@const color = ROLE_COLORS[role] || '#6b7594'} + {@const isActive = transcriptFilters.size === 0 || transcriptFilters.has(role)} + + {/each} +
- {#each turns as turn, i} + {#each turns.filter(t => transcriptFilters.size === 0 || transcriptFilters.has(t.role)) as turn}
Option 0; if has_tokens { let model_name = detected_model.as_deref().unwrap_or("unknown"); + // input_tokens from the API includes cache_read and cache_write, + // subtract to get fresh (non-cached) input only + let fresh_input = (batch_input - batch_cache_read - batch_cache_write).max(0); let batch_cost = crate::pricing::estimate_cost( model_name, - batch_input, + fresh_input, batch_output, batch_cache_read, batch_cache_write, @@ -134,7 +137,7 @@ pub async fn handle_stream( WHERE id = $1", ) .bind(session_db_id) - .bind(batch_input) + .bind(fresh_input) .bind(batch_output) .bind(batch_cache_read) .bind(batch_cache_write) From a74c45dfdc905955f455161ac0133d83d71a148a Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 26 Mar 2026 12:41:04 +0100 Subject: [PATCH 5/6] fix: use whole dollar format on cumulative cost chart, fix double-counted input tokens --- .../008_fix_double_counted_input_tokens.sql | 14 ++++++++++++++ .../components/session-detail/SessionCharts.svelte | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 crates/tracevault-server/migrations/008_fix_double_counted_input_tokens.sql diff --git a/crates/tracevault-server/migrations/008_fix_double_counted_input_tokens.sql b/crates/tracevault-server/migrations/008_fix_double_counted_input_tokens.sql new file mode 100644 index 0000000..fe86f95 --- /dev/null +++ b/crates/tracevault-server/migrations/008_fix_double_counted_input_tokens.sql @@ -0,0 +1,14 @@ +-- Fix input_tokens that were double-counted (included cache_read + cache_write tokens). +-- Subtract cache tokens to get fresh (non-cached) input only. +UPDATE sessions_v2 +SET input_tokens = GREATEST(input_tokens - cache_read_tokens - cache_write_tokens, 0), + total_tokens = GREATEST(input_tokens - cache_read_tokens - cache_write_tokens, 0) + + output_tokens + cache_read_tokens + cache_write_tokens +WHERE input_tokens > 0 + AND (cache_read_tokens > 0 OR cache_write_tokens > 0); + +-- Reset estimated_cost_usd to 0 so it gets recalculated by the pricing sync. +-- The pricing sync runs on startup and will recalculate all affected sessions. +UPDATE sessions_v2 +SET estimated_cost_usd = 0 +WHERE estimated_cost_usd > 0; diff --git a/web/src/lib/components/session-detail/SessionCharts.svelte b/web/src/lib/components/session-detail/SessionCharts.svelte index b723f72..d4f8c3d 100644 --- a/web/src/lib/components/session-detail/SessionCharts.svelte +++ b/web/src/lib/components/session-detail/SessionCharts.svelte @@ -133,7 +133,7 @@ scales: { y: { ticks: { - callback: (value: string | number) => `$${Number(value).toFixed(3)}` + callback: (value: string | number) => `$${Math.round(Number(value))}` } } }, From 519408470f1c057e6d4aa99496056f515dfd2b9a Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 26 Mar 2026 12:51:55 +0100 Subject: [PATCH 6/6] feat: add help tooltips with descriptions to all charts and stat cards --- web/src/lib/components/HelpTip.svelte | 20 ++++++ web/src/lib/components/StatCard.svelte | 16 ++++- .../dashboard/CacheSavingsCard.svelte | 4 +- .../dashboard/ComplianceCard.svelte | 4 +- .../lib/components/dashboard/KpiCard.svelte | 7 +- .../dashboard/SessionQualityBar.svelte | 8 ++- .../session-detail/SessionCharts.svelte | 7 +- .../session-detail/SessionSummaryStats.svelte | 70 +++++++++++++++++-- .../routes/orgs/[slug]/analytics/+page.svelte | 24 ++++--- .../[slug]/analytics/attribution/+page.svelte | 13 ++-- .../orgs/[slug]/analytics/cost/+page.svelte | 15 ++-- .../orgs/[slug]/analytics/models/+page.svelte | 7 +- .../[slug]/analytics/sessions/+page.svelte | 12 ++-- .../orgs/[slug]/analytics/tokens/+page.svelte | 8 ++- .../routes/orgs/[slug]/dashboard/+page.svelte | 4 ++ 15 files changed, 172 insertions(+), 47 deletions(-) create mode 100644 web/src/lib/components/HelpTip.svelte diff --git a/web/src/lib/components/HelpTip.svelte b/web/src/lib/components/HelpTip.svelte new file mode 100644 index 0000000..915e9ba --- /dev/null +++ b/web/src/lib/components/HelpTip.svelte @@ -0,0 +1,20 @@ + + + + + ? + + + + {text} + + + diff --git a/web/src/lib/components/StatCard.svelte b/web/src/lib/components/StatCard.svelte index 2da472f..cbac94e 100644 --- a/web/src/lib/components/StatCard.svelte +++ b/web/src/lib/components/StatCard.svelte @@ -1,5 +1,6 @@
@@ -23,6 +25,18 @@
{label} + {#if tooltip} + + + ? + + + + {tooltip} + + + + {/if}
{value}
{#if secondary} diff --git a/web/src/lib/components/dashboard/CacheSavingsCard.svelte b/web/src/lib/components/dashboard/CacheSavingsCard.svelte index 251d1c6..724e329 100644 --- a/web/src/lib/components/dashboard/CacheSavingsCard.svelte +++ b/web/src/lib/components/dashboard/CacheSavingsCard.svelte @@ -1,4 +1,6 @@