feat(dashboard): stream in-flight response bodies into live logs and interactions#495
Conversation
…interactions While a streamed completion runs, the audit stream observer now publishes throttled audit.stream live events carrying the partially reconstructed response body, so connected dashboards render the response chunk-by-chunk instead of waiting for the stream to finish. Previews are published at most every 500ms, only when new content accumulated, and only while a dashboard subscriber is connected; the broker fans partial bodies out live but strips them from the replay ring, so retention stays bounded. The request log keeps its in-progress pulse while a partial body is present, shows a streaming badge on the Response pane, and shows spinners instead of "not captured" for panes of in-flight entries. The Interactions drawer now renders live entries directly from the preview data (previously "No interaction data available" until flush), re-renders as chunks arrive, shows a progress spinner, and hydrates the persisted thread once the entry flushes. Also moves the auditlog enrichment API into enrich.go and routes every context lookup through the existing entryFromContext helper, removing the repeated boilerplate. Closes the remaining piece of #290. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Caution Review failedAn error occurred during the review process. Please try again later. 📝 WalkthroughWalkthroughThis PR adds throttled ChangesBackend live-stream preview and enrichment refactor
Dashboard live streaming and pending/status UI
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Stream as SSE stream
participant Observer as StreamLogObserver
participant Broker as live.Broker
participant LiveLogs as live-logs.js
participant Drawer as conversation-drawer.js
Stream->>Observer: OnJSONEvent(chunk)
Observer->>Broker: PublishLiveEvent(audit.stream, partial body)
Broker->>LiveLogs: broadcast live update
LiveLogs->>LiveLogs: mergeLiveAuditEntry(_response_partial=true)
LiveLogs->>Drawer: notifyLiveConversation(entry)
Drawer->>Drawer: refreshLiveConversation(entry)
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/admin/dashboard/static/js/modules/audit-list.js (1)
726-753: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winGuard
streamingwith the live-state completion check
streamingcan remain true whenever_response_partialis left set, even after the entry has moved past the streaming phase. Reuse the same completion guard used byauditEntryLiveInProgress:🐛 Proposed fix
- streaming: !!(entry && entry._response_partial && data && data.response_body), + streaming: !!(entry && entry._response_partial && data && data.response_body && this.auditEntryLiveInProgress(entry)),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/admin/dashboard/static/js/modules/audit-list.js` around lines 726 - 753, The auditResponsePane() logic sets streaming based only on _response_partial, which can stay true after the live response phase has finished. Update the streaming flag to use the same completion guard as auditEntryLiveInProgress() so it only reports streaming while the entry is still actively in progress, and keep the response pane state aligned with the live-state checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/admin/dashboard/static/js/modules/conversation-drawer.js`:
- Around line 143-151: The conversationLiveWaiting method currently hard-codes a
rank < 30 check, which is tightly coupled to the liveAuditStateRank ordering.
Replace the magic number with a shared named constant or helper owned by the
live-logs/live state ranking module, such as a completion threshold or an
isLiveStateComplete-style function, and update conversationLiveWaiting to use
that exported symbol instead of comparing against a raw numeric value.
- Around line 110-116: The _conversationEntryLivePending method still has a
fallback branch for entry._live_pending even though dashboard.js guarantees
auditEntryLiveDetailPending is available before
dashboardConversationDrawerModule loads. Update _conversationEntryLivePending to
call auditEntryLiveDetailPending directly and remove the dead typeof check and
fallback branch, keeping the logic localized to the existing conversation drawer
module method.
In `@internal/admin/dashboard/static/js/modules/conversation-drawer.test.cjs`:
- Around line 44-59: The local test stub for auditEntryLiveDetailPending is too
simplified and does not match the real live-logs.js behavior, so update the stub
in conversation-drawer.test.cjs to mirror the production logic more closely.
Make it handle the audit.failed special case and the _live_state checks for
audit.flushed and audit.detail, using the same entry shape and state handling as
the real implementation so conversation-drawer.js tests exercise the correct
behavior.
In `@internal/admin/dashboard/templates/page-audit-logs.html`:
- Around line 210-214: Make the live status updates announceable to assistive
tech by adding an appropriate live region to the conversation-live-status
container. Update the markup around conversationLiveWaiting() /
conversationLiveStatusText() to include role="status" and/or aria-live="polite"
so screen readers are notified when the streaming status text changes, while
keeping the existing x-show and x-text behavior intact.
In `@internal/auditlog/enrich.go`:
- Around line 287-303: EnrichEntryWithError currently drops ErrorMessage and
ErrorCode when entry.Data is nil, so initialize entry.Data before setting error
details in EnrichEntryWithError and keep the existing error-type assignment and
publishLiveAuditUpdate flow intact. Use entryFromContext and the entry.Data
access in this function to ensure the audit entry always has a data container
before storing errorMessage/errorCode, including early-failure cases.
In `@internal/auditlog/stream_observer.go`:
- Around line 87-104: The live preview path in
StreamLogObserver.maybePublishLivePreview still rebuilds the full response body
on every published tick via applyResponseBody, which can become expensive for
very long streams. Update the preview update flow so applyResponseBody is not
doing a full reconstruction each time; instead reuse or incrementally update the
existing preview state while keeping the same throttle and live-subscriber
gating behavior in maybePublishLivePreview and any related preview publishing
helpers.
---
Outside diff comments:
In `@internal/admin/dashboard/static/js/modules/audit-list.js`:
- Around line 726-753: The auditResponsePane() logic sets streaming based only
on _response_partial, which can stay true after the live response phase has
finished. Update the streaming flag to use the same completion guard as
auditEntryLiveInProgress() so it only reports streaming while the entry is still
actively in progress, and keep the response pane state aligned with the
live-state checks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 233d1e81-ae00-4587-8ae1-e1d066a5ad13
📒 Files selected for processing (19)
CLAUDE.mdinternal/admin/dashboard/static/css/dashboard.cssinternal/admin/dashboard/static/js/dashboard.jsinternal/admin/dashboard/static/js/modules/audit-list.jsinternal/admin/dashboard/static/js/modules/audit-list.test.cjsinternal/admin/dashboard/static/js/modules/conversation-drawer.jsinternal/admin/dashboard/static/js/modules/conversation-drawer.test.cjsinternal/admin/dashboard/static/js/modules/live-logs.jsinternal/admin/dashboard/static/js/modules/live-logs.test.cjsinternal/admin/dashboard/templates/audit-pane.htmlinternal/admin/dashboard/templates/page-audit-logs.htmlinternal/auditlog/auditlog.gointernal/auditlog/enrich.gointernal/auditlog/logger.gointernal/auditlog/middleware.gointernal/auditlog/stream_observer.gointernal/auditlog/stream_observer_test.gointernal/live/broker.gointernal/live/broker_test.go
💤 Files with no reviewable changes (1)
- internal/auditlog/middleware.go
| func (o *StreamLogObserver) maybePublishLivePreview() { | ||
| if o.live == nil || o.entry == nil || o.entry.Data == nil { | ||
| return | ||
| } | ||
| if o.builder.contentLen == o.lastPreviewLen { | ||
| return | ||
| } | ||
| if time.Since(o.lastPreviewAt) < livePreviewInterval { | ||
| return | ||
| } | ||
| o.lastPreviewAt = time.Now() | ||
| if !liveSubscribed(o.live) { | ||
| return | ||
| } | ||
| o.lastPreviewLen = o.builder.contentLen | ||
| o.applyResponseBody() | ||
| o.live.PublishLiveEvent(LiveEventAuditStream, o.entry) | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff
Solid throttled-preview design; minor scalability note for very long streams.
The throttle ordering (content-length check → time window → subscriber check → publish) correctly amortizes the subscriber-presence check and lets a newly-connected subscriber catch up on the next tick, as confirmed by the accompanying tests. One thing worth keeping in mind: applyResponseBody fully reconstructs the response body from the builder on every published tick (every 500ms while a subscriber is connected), so total preview-rebuild cost grows with stream length. This is bounded by the throttle and by HasLiveSubscribers, so it's unlikely to be a problem for typical completions, but could add up for very long streamed generations with many concurrent dashboard viewers.
Also applies to: 113-122
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/auditlog/stream_observer.go` around lines 87 - 104, The live preview
path in StreamLogObserver.maybePublishLivePreview still rebuilds the full
response body on every published tick via applyResponseBody, which can become
expensive for very long streams. Update the preview update flow so
applyResponseBody is not doing a full reconstruction each time; instead reuse or
incrementally update the existing preview state while keeping the same throttle
and live-subscriber gating behavior in maybePublishLivePreview and any related
preview publishing helpers.
There was a problem hiding this comment.
Pull request overview
This PR adds a new audit.stream live event to deliver throttled, in-flight reconstructed streamed response bodies to connected dashboards, enabling near-realtime “chunk-by-chunk” UI updates while keeping broker retention bounded by stripping partial bodies from replay/active snapshots. It also refactors the auditlog enrichment helpers out of the large middleware file into a dedicated enrich.go.
Changes:
- Backend: publish throttled
audit.streamlive previews only when a dashboard subscriber is connected; ensure replay retention strips partial bodies and does not incorrectly mark them as captured. - Frontend: merge
audit.streaminto live log rows with a_response_partiallifecycle; add pending/streaming UI states for request/response panes and live rendering/hydration behavior for the Interactions drawer. - Refactor: move auditlog enrichment API helpers from
middleware.gointoenrich.gowhile preserving the symbol set.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/live/broker.go | Adds audit.stream support, HasLiveSubscribers, and partial-body stripping semantics for retention. |
| internal/live/broker_test.go | Tests for audit.stream preview behavior and subscriber reporting. |
| internal/auditlog/stream_observer.go | Publishes throttled partial-response previews via audit.stream during SSE observation. |
| internal/auditlog/stream_observer_test.go | New tests validating throttling, content gating, subscriber gating, and Responses API previews. |
| internal/auditlog/middleware.go | Removes enrichment helpers after migration to enrich.go. |
| internal/auditlog/logger.go | Adds HasLiveSubscribers passthrough to the live publisher when supported. |
| internal/auditlog/enrich.go | New home for context-based and direct-entry enrichment helpers. |
| internal/auditlog/auditlog.go | Introduces LiveEventAuditStream and the LiveSubscriberReporter interface. |
| internal/admin/dashboard/templates/page-audit-logs.html | Updates Interactions empty-state logic and adds live status block. |
| internal/admin/dashboard/templates/audit-pane.html | Adds streaming badge and pending spinner UI for audit panes. |
| internal/admin/dashboard/static/js/modules/live-logs.test.cjs | Tests for audit.stream merge lifecycle and live conversation drawer notifications. |
| internal/admin/dashboard/static/js/modules/live-logs.js | Merges audit.stream, manages _response_partial, and notifies Interactions drawer on live merges. |
| internal/admin/dashboard/static/js/modules/conversation-drawer.test.cjs | New tests covering live rendering, chunk re-renders, flush hydration, and spinner states. |
| internal/admin/dashboard/static/js/modules/conversation-drawer.js | Renders live entries locally, re-renders on live merges, and hydrates persisted thread on flush. |
| internal/admin/dashboard/static/js/modules/audit-list.test.cjs | Adds tests for in-progress behavior and pending/streaming pane states. |
| internal/admin/dashboard/static/js/modules/audit-list.js | Adds pending/streaming state derivation for request/response panes and in-progress logic tweak. |
| internal/admin/dashboard/static/js/dashboard.js | Adds conversationLiveEntryId to dashboard state. |
| internal/admin/dashboard/static/css/dashboard.css | Styles for pane pending/streaming and conversation live status. |
| CLAUDE.md | Documents new live streamed-response behavior under dashboard live logs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const data = entry && entry.data ? entry.data : null; | ||
| const empty = !data || (!data.request_headers && !data.request_body); | ||
| const pending = empty && this.auditEntryLiveInProgress(entry); | ||
|
|
| const data = entry && entry.data ? entry.data : null; | ||
| const errorMessage = this.auditEntryErrorMessage(entry); | ||
| const empty = !data || (!errorMessage && !data.response_headers && !data.response_body); | ||
| const pending = empty && this.auditEntryLiveInProgress(entry); |
| if (state === 'audit.flushed' || state === 'audit.detail') { | ||
| this.conversationLiveEntryId = ''; | ||
| const requestToken = ++this.conversationRequestToken; | ||
| this.fetchConversation(entry.id, requestToken); | ||
| return; | ||
| } |
- share liveAuditStateSettled instead of a raw rank comparison in the Interactions drawer, and drop the dead _live_pending fallback (the presence guard stays: modules mix optionally, matching every other cross-module call) - drawer tests load the real live-logs module instead of drifting stubs - announce the drawer's live status to assistive tech (role=status) - EnrichEntryWithError creates the data container when missing instead of silently dropping error message/code - the Response pane streaming badge uses the same completion guard as the in-progress pulse so a stale partial flag cannot keep it lit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/admin/dashboard/static/js/modules/live-logs.js (1)
211-215: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse the new
liveAuditStateSettledhelper instead of the raw rank literal30.This file defines
liveAuditStateSettled(state)(Lines 306-311) specifically to avoid hardcoding the "completed" rank threshold, andconversation-drawer.jswas updated to use it per the latest commit. This call site — in the very file that owns the ranking — still hardcodes30.♻️ Proposed fix
if (eventType === 'audit.stream') { patch._response_partial = true; - } else if (this.liveAuditStateRank(eventType) >= 30) { + } else if (this.liveAuditStateSettled(eventType)) { patch._response_partial = false; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/admin/dashboard/static/js/modules/live-logs.js` around lines 211 - 215, The live audit state check still hardcodes the settled threshold with the literal 30 in the live logs patching logic, so update that call site to use the existing liveAuditStateSettled(state) helper instead. In live-logs.js, replace the raw rank comparison in the eventType handling with the helper so the settled-state decision stays centralized alongside liveAuditStateRank and liveAuditStateSettled.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@internal/admin/dashboard/static/js/modules/live-logs.js`:
- Around line 211-215: The live audit state check still hardcodes the settled
threshold with the literal 30 in the live logs patching logic, so update that
call site to use the existing liveAuditStateSettled(state) helper instead. In
live-logs.js, replace the raw rank comparison in the eventType handling with the
helper so the settled-state decision stays centralized alongside
liveAuditStateRank and liveAuditStateSettled.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: e8a1485e-8c11-4cee-997f-d658409ebeab
📒 Files selected for processing (7)
internal/admin/dashboard/static/js/modules/audit-list.jsinternal/admin/dashboard/static/js/modules/audit-list.test.cjsinternal/admin/dashboard/static/js/modules/conversation-drawer.jsinternal/admin/dashboard/static/js/modules/conversation-drawer.test.cjsinternal/admin/dashboard/static/js/modules/live-logs.jsinternal/admin/dashboard/templates/page-audit-logs.htmlinternal/auditlog/enrich.go
…eset Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What
Completes the remaining piece of #290: while a streamed completion is running, the dashboard now shows the response chunk-by-chunk in near-realtime — in the request log's Response pane and in the Interactions drawer — instead of waiting for the stream to finish. In-flight entries also get progress spinners wherever the UI previously showed "not captured" / "No interaction data available".
User-visible behavior
With
LOGGING_ENABLED=true+LOGGING_LOG_BODIES=trueand dashboard live logs on (all defaults except body logging):How
audit.streamlive event:StreamLogObserverpublishes the partially reconstructed response body while the stream runs. Publishes are throttled to one per 500 ms per stream, skipped when no new content accumulated, and skipped entirely while no dashboard subscriber is connected (Broker.HasLiveSubscribers, surfaced through the logger via the newLiveSubscriberReporterinterface). A subscriber connecting mid-stream catches up on the next tick because every preview carries the full accumulated body.response_body_partialon fan-out copies, never markedresponse_body_captured, since the body is not persisted yet), so broker memory stays bounded exactly as before._response_partiallifecycle in the live merge (audit.streamsets it, terminal states clear it), streaming/pending states on the audit panes, and a live rendering mode for the Interactions drawer that switches to the persisted fetch on flush.No new configuration; no behavior change when body logging or live logs are disabled. Non-streamed (and passthrough non-SSE) requests are unaffected.
Refactor (same PR, no behavior change)
The auditlog enrichment API moved from
middleware.go(900+ lines) into a newenrich.go, and all ~15EnrichEntryWith*helpers now use the existingentryFromContexthelper instead of repeating the context-get + type-assert boilerplate. Symbol set is identical before/after.Testing
HasLiveSubscribers). Fullmake test-race, e2e, and lint pass._response_partialmerge lifecycle, pane pending/streaming states, and a newconversation-drawer.test.cjscovering live rendering, chunk re-renders, flush hydration, and spinner states (431 dashboard tests pass).audit.started → audit.updated → audit.stream×9(accumulated content growing ~2×/s,response_body_partial: true)→ audit.completed(final body + usage, no partial flag)→ audit.flushed; fresh subscribers replay without partial bodies; the proxied client stream is byte-identical and unaffected.Refs #290
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests