Conversation
📝 WalkthroughWalkthroughAdds admin audit logging: new admin API routes and handlers, an auditlog reader abstraction with SQLite/Postgres/Mongo implementations and indexes, admin dashboard UI (templates, CSS, modular JS) for browsing/filtering/paginating logs and viewing threaded conversations, and regenerated Swagger/docs. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Admin UI
participant Handler as Admin Handler
participant Reader as Audit Reader
participant DB as Database (SQLite/PG/Mongo)
Client->>Handler: GET /admin/api/v1/audit/log?filters
Handler->>Reader: GetLogs(ctx, LogQueryParams)
Reader->>DB: Query/Aggregate logs with filters & pagination
DB-->>Reader: Rows/Documents
Reader-->>Handler: LogListResult (entries + total/limit/offset)
Handler-->>Client: JSON response (entries + metadata)
sequenceDiagram
participant Client as Admin UI (Conversation)
participant Handler as Admin Handler
participant Reader as Audit Reader
participant DB as Database
Client->>Handler: GET /admin/api/v1/audit/conversation?log_id=X&limit=Y
Handler->>Reader: GetConversation(ctx, logID, limit)
Reader->>DB: GetLogByID(seed)
DB-->>Reader: seed LogEntry
Reader->>DB: findByPreviousResponseID (walk backwards)
DB-->>Reader: previous entries
Reader->>DB: findByResponseID (walk forwards)
DB-->>Reader: subsequent entries
Reader-->>Handler: ConversationResult (anchor + ordered entries)
Handler-->>Client: JSON response (conversation thread)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
There was a problem hiding this comment.
Actionable comments posted: 10
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cmd/gomodel/docs/swagger.json`:
- Around line 135-138: The OpenAPI parameter definition for "stream" is declared
as "type": "string" but the backend uses LogQueryParams with Stream *bool;
update the swagger parameter for name "stream" (in: query) to use "type":
"boolean" so the spec matches the LogQueryParams boolean field and generated
clients will send boolean values.
In `@internal/admin/dashboard/static/css/dashboard.css`:
- Around line 1207-1211: Replace the deprecated usage of "word-break:
break-word" in the .audit-json-body selector by removing that property and
relying on "overflow-wrap: anywhere" (already present) to achieve the same
behavior; update the .audit-json-body rule to drop "word-break: break-word" and
keep only "white-space: pre-wrap" and "overflow-wrap: anywhere" so you eliminate
the deprecated property while preserving intended wrapping.
- Around line 1377-1384: Update the .chat-content CSS: remove the unnecessary
quotes around the Inter font name in the font-family list and replace the
deprecated property word-break: break-word with overflow-wrap: break-word (keep
other properties like white-space and line-height intact); locate these changes
in the .chat-content rule to ensure font-family becomes Inter, -apple-system,
BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif and word wrapping is handled
via overflow-wrap: break-word.
In `@internal/admin/dashboard/static/js/modules/audit-list.js`:
- Around line 70-75: statusCodeClass currently treats null/undefined/non-numeric
inputs as success; update the statusCodeClass function to first detect invalid
or non-numeric statusCode (e.g., using Number.isFinite or typeof checks) and
return a distinct class like 'status-unknown' (or 'status-neutral') for those
cases before the numeric comparisons for 500/400/300, so unknown states are not
rendered as 'status-success'.
In `@internal/admin/dashboard/templates/index.html`:
- Around line 347-350: The filter inputs rely only on placeholders and lack
accessible names; add explicit labels or aria-label attributes and corresponding
ids to each control (the inputs bound to auditSearch, auditModel, auditProvider,
auditPath and any similar controls in the 354-379 range) so screen readers and
voice-control tools can identify them, ensuring you either add <label for="...">
elements pointing to new id attributes or set descriptive aria-label values
(e.g., "Search by request ID", "Model filter", "Provider filter", "Path filter")
and keep the existing x-model and `@input.debounce.300ms`="fetchAuditLog(true)"
bindings intact.
- Around line 397-404: The icon-only button with class
"audit-conversation-trigger" relies on title for accessibility but needs an
explicit accessible name; add an aria-label (e.g., aria-label="Open
conversation") to the button element alongside the existing title, ensuring it
is present on the element that calls openConversation(entry,
$el.closest('details'), true) and visible when canShowConversation(entry) is
true; keep the same descriptive text as the title (or use your localization
helper) so screen readers have a clear name for the control.
In `@internal/admin/handler.go`:
- Around line 407-444: AuditConversation currently parses limit with
strconv.Atoi and passes it to h.auditReader.GetConversation without validating
bounds; update AuditConversation to validate the parsed limit (after
strconv.Atoi) and return a core.NewInvalidRequestError for out-of-range values
(e.g., enforce limit >= 1 and <= 1000 or another agreed-upon max) before calling
h.auditReader.GetConversation, mentioning the parameter name "limit" in the
error; reference the AuditConversation function, the limit variable,
strconv.Atoi, and the GetConversation call when making the change.
In `@internal/auditlog/reader_mongodb.go`:
- Around line 317-403: The three functions GetLogByID, findByResponseID, and
findByPreviousResponseID repeat the same anonymous scan struct; extract that
into a shared named type (e.g., mongoLogRow) with the existing bson tags and add
a helper method (e.g., toLogEntry) to convert mongoLogRow -> *LogEntry, then
replace the inline struct declarations in GetLogByID, findByResponseID, and
findByPreviousResponseID with the new mongoLogRow type and call toLogEntry
before returning.
In `@internal/auditlog/reader_sqlite.go`:
- Around line 258-275: The JSON extraction in findByResponseID (and any
analogous lookup such as the method that queries json_extract(data,
'$.request_body.previous_response_id')) will cause full table scans on large
datasets; add SQLite expression indexes for these JSON paths (e.g., an index on
json_extract(data, '$.response_body.id') and one on json_extract(data,
'$.request_body.previous_response_id')) to speed lookups, and include their
CREATE INDEX statements in your migration or DB initialization so the queries in
SQLiteReader (findByResponseID and the previous-response lookup) use the
indexes.
- Around line 167-190: The loop that builds the thread currently prepends
entries using append([]*LogEntry{parent}, thread...), causing O(n²) behavior;
change it to append parents at the end (thread = append(thread, parent)) while
still marking seen[parent.ID] and advancing current = parent, and then rely on
the existing sort-by-timestamp step (or reverse once after the loop) to produce
the final chronological order; update any logic that assumes the list was built
front-to-back (e.g., usages of thread, extractPreviousResponseID,
findByResponseID, anchor, limit) to accommodate the new append-then-sort/reverse
approach.
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (31)
AGENTS.mdcmd/gomodel/docs/docs.gocmd/gomodel/docs/swagger.jsoninternal/admin/dashboard/dashboard.gointernal/admin/dashboard/dashboard_test.gointernal/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/charts.jsinternal/admin/dashboard/static/js/modules/conversation-drawer.jsinternal/admin/dashboard/static/js/modules/conversation-helpers.jsinternal/admin/dashboard/static/js/modules/date-picker.jsinternal/admin/dashboard/static/js/modules/usage.jsinternal/admin/dashboard/templates/index.htmlinternal/admin/dashboard/templates/layout.htmlinternal/admin/handler.gointernal/admin/handler_test.gointernal/app/app.gointernal/auditlog/conversation_helpers.gointernal/auditlog/reader.gointernal/auditlog/reader_factory.gointernal/auditlog/reader_helpers.gointernal/auditlog/reader_mongodb.gointernal/auditlog/reader_postgresql.gointernal/auditlog/reader_sqlite.gointernal/auditlog/store_mongodb.gointernal/auditlog/store_postgresql.gointernal/auditlog/store_sqlite.gointernal/server/http.gointernal/server/http_test.gotests/e2e/admin_test.go
| func (r *SQLiteReader) findByResponseID(ctx context.Context, responseID string) (*LogEntry, error) { | ||
| query := `SELECT id, timestamp, duration_ns, model, provider, status_code, request_id, | ||
| client_ip, method, path, stream, error_type, data | ||
| FROM audit_logs | ||
| WHERE json_extract(data, '$.response_body.id') = ? | ||
| ORDER BY timestamp ASC | ||
| LIMIT 1` | ||
|
|
||
| rows, err := r.db.QueryContext(ctx, query, responseID) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to query audit log by response id: %w", err) | ||
| } | ||
| defer rows.Close() | ||
| if !rows.Next() { | ||
| return nil, nil | ||
| } | ||
| return scanSQLiteLogEntry(rows) | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Verify JSON path extraction performance on large datasets.
The queries use json_extract(data, '$.response_body.id') and json_extract(data, '$.request_body.previous_response_id') for conversation threading. Without a computed/virtual column or expression index, these queries will perform full table scans.
Consider adding expression indexes if conversation lookups become a performance bottleneck:
CREATE INDEX idx_audit_logs_response_id ON audit_logs(json_extract(data, '$.response_body.id'));
CREATE INDEX idx_audit_logs_prev_response_id ON audit_logs(json_extract(data, '$.request_body.previous_response_id'));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/auditlog/reader_sqlite.go` around lines 258 - 275, The JSON
extraction in findByResponseID (and any analogous lookup such as the method that
queries json_extract(data, '$.request_body.previous_response_id')) will cause
full table scans on large datasets; add SQLite expression indexes for these JSON
paths (e.g., an index on json_extract(data, '$.response_body.id') and one on
json_extract(data, '$.request_body.previous_response_id')) to speed lookups, and
include their CREATE INDEX statements in your migration or DB initialization so
the queries in SQLiteReader (findByResponseID and the previous-response lookup)
use the indexes.
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (1)
internal/auditlog/reader_sqlite.go (1)
258-294: 🧹 Nitpick | 🔵 TrivialConsider adding expression indexes for JSON path queries.
The
json_extractqueries on$.response_body.idand$.request_body.previous_response_idwill perform full table scans. While acceptable for low-volume admin queries, this could become a bottleneck with large audit logs.-- Suggested indexes for conversation threading performance CREATE INDEX IF NOT EXISTS idx_audit_logs_response_id ON audit_logs(json_extract(data, '$.response_body.id')); CREATE INDEX IF NOT EXISTS idx_audit_logs_prev_response_id ON audit_logs(json_extract(data, '$.request_body.previous_response_id'));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/auditlog/reader_sqlite.go` around lines 258 - 294, The JSON path lookups in findByResponseID and findByPreviousResponseID cause full table scans; add persistent expression indexes for json_extract(data, '$.response_body.id') and json_extract(data, '$.request_body.previous_response_id') in your DB initialization/migration so those queries use indexes (create indexes if not exists, e.g., idx_audit_logs_response_id and idx_audit_logs_prev_response_id) and ensure the migration runs before these reader methods are used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cmd/gomodel/docs/swagger.json`:
- Around line 715-727: Update the OpenAPI 2.0 declarations: for the
/v1/chat/completions operation replace the single invalid produces entry "json
text/event-stream" with two separate produces entries "application/json" and
"text/event-stream"; for the /v1/files/{id}/content response change the response
schema type from "string" to "file" so the binary download is correctly
declared; and for the POST operation on /v1/batches/{id}/cancel add an
appropriate consumes array (e.g., "application/json" or the actual request
content type) to the operation object so it conforms to Swagger 2.0
requirements.
In `@internal/admin/dashboard/static/css/dashboard.css`:
- Around line 1135-1150: The stylesheet uses a hardcoded max-height: 2200px for
.audit-entry[open] .audit-entry-details which can make the expand animation
jumpy for taller content; replace this by removing the arbitrary large
max-height and either (a) set a reasonable bounded value on .audit-entry-details
and document the limitation, or (b) implement a JS-driven height transition: on
open, measure the element's scrollHeight and apply that value as an inline
max-height, then remove or reset it after the transition to allow natural
sizing; update the logic around the .audit-entry-details and .audit-entry[open]
.audit-entry-details selectors to use the JS-managed inline style for smooth
animations on large entries.
In `@internal/admin/dashboard/static/js/modules/audit-list.js`:
- Around line 102-110: The copyAuditJSON function currently only logs errors to
the console; update its catch block to provide brief user-visible feedback on
failure by either (a) changing the originating button text/icon temporarily
(e.g., set button.innerText = 'Copy failed' then restore after ~2s) or (b)
invoking an existing toast/notification helper (e.g., call showToast or dispatch
a custom 'notify' event) so the user sees the failure; keep the console.error
for debugging, ensure you reference copyAuditJSON and formatJSON in your change,
and restore any temporary UI change after a short timeout.
- Around line 63-68: The condition in formatDurationNs redundantly checks both
ns == null and ns === undefined; replace the two-part check with a single loose
equality check (ns == null) to cover null and undefined, keeping the rest of the
function intact so it still returns '-' for missing values and formats values
for µs/ms/s as before; update the method name formatDurationNs accordingly.
In `@internal/admin/dashboard/templates/index.html`:
- Line 479: The overlay div with class "conversation-overlay" (the element using
x-show="conversationOpen" and `@click`="closeConversation()") should be marked as
non‑interactive for assistive tech; add aria-hidden="true" to that element so
screen readers ignore the visual backdrop while preserving its click behavior to
close the conversation.
- Around line 480-505: Add ARIA attributes to the conversation drawer element
and its controls to treat it as an accessible dialog: on the aside with class
"conversation-drawer" (which uses conversationOpen and closeConversation()) add
role="dialog", aria-modal="true" and bind aria-hidden based on conversationOpen;
add aria-labelledby pointing to the <h3> header (or give the header an id) and
aria-describedby pointing to the main content container (the conversation-thread
or the appropriate empty-state elements) so screen readers announce the title
and contents; ensure the Close button has an accessible name (aria-label) and
that focus management is implemented when opening/closing the drawer (set
initial focus to the dialog or first focusable element and return focus to the
trigger on close) while keeping existing bindings like conversationMessages,
conversationError, conversationLoading and conversationAnchorID intact.
In `@internal/auditlog/reader_mongodb.go`:
- Around line 36-51: The toLogEntry mapping in mongoLogRow returns LogEntry.Data
verbatim, risking leakage of sensitive headers; update mongoLogRow.toLogEntry to
run a redaction pass over Data's "request_headers" and "response_headers" keys
before returning (e.g., call a helper like redactSensitiveHeaders or similar) to
remove/obfuscate Authorization, Cookie, X-API-Key and other sensitive header
names while leaving other fields intact; ensure the helper is deterministic and
handles nil/missing keys and non-map values gracefully so all legacy/untrusted
rows are sanitized prior to creating the LogEntry.
---
Duplicate comments:
In `@internal/auditlog/reader_sqlite.go`:
- Around line 258-294: The JSON path lookups in findByResponseID and
findByPreviousResponseID cause full table scans; add persistent expression
indexes for json_extract(data, '$.response_body.id') and json_extract(data,
'$.request_body.previous_response_id') in your DB initialization/migration so
those queries use indexes (create indexes if not exists, e.g.,
idx_audit_logs_response_id and idx_audit_logs_prev_response_id) and ensure the
migration runs before these reader methods are used.
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (14)
AGENTS.mdcmd/gomodel/docs/docs.gocmd/gomodel/docs/swagger.jsoninternal/admin/dashboard/static/css/dashboard.cssinternal/admin/dashboard/static/js/modules/audit-list.jsinternal/admin/dashboard/templates/index.htmlinternal/admin/handler.gointernal/app/app.gointernal/auditlog/reader_mongodb.gointernal/auditlog/reader_sqlite.gointernal/core/batch.gointernal/core/types.gointernal/server/handlers.gointernal/server/http.go
| .audit-entry-details { | ||
| border-top: 1px solid transparent; | ||
| padding: 0 12px; | ||
| background: var(--bg-surface); | ||
| max-height: 0; | ||
| opacity: 0; | ||
| overflow: hidden; | ||
| transition: max-height 0.12s ease-out, opacity 0.08s linear, padding 0.12s ease-out, border-color 0.12s ease-out; | ||
| } | ||
|
|
||
| .audit-entry[open] .audit-entry-details { | ||
| border-top-color: var(--border); | ||
| padding: 12px; | ||
| max-height: 2200px; | ||
| opacity: 1; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider reducing the hardcoded max-height transition value.
The max-height: 2200px is an arbitrary large value used for the expand/collapse animation. While functional, if actual content exceeds this, the animation may appear jumpy. Consider using a JavaScript-based height calculation for smoother transitions on very large entries, or document this as a known limitation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/admin/dashboard/static/css/dashboard.css` around lines 1135 - 1150,
The stylesheet uses a hardcoded max-height: 2200px for .audit-entry[open]
.audit-entry-details which can make the expand animation jumpy for taller
content; replace this by removing the arbitrary large max-height and either (a)
set a reasonable bounded value on .audit-entry-details and document the
limitation, or (b) implement a JS-driven height transition: on open, measure the
element's scrollHeight and apply that value as an inline max-height, then remove
or reset it after the transition to allow natural sizing; update the logic
around the .audit-entry-details and .audit-entry[open] .audit-entry-details
selectors to use the JS-managed inline style for smooth animations on large
entries.
There was a problem hiding this comment.
Actionable comments posted: 2
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/dashboard.js (1)
79-107:⚠️ Potential issue | 🟠 MajorFetch audit data during initial audit-route load.
auditis parsed as a valid route, but audit fetch runs only inpopstate/navigate; direct first-load on audit can stay empty.🛠️ Proposed fix
init() { @@ - this.fetchAll(); + this.fetchAll(); + if (this.page === 'audit' && typeof this.fetchAuditLog === 'function') { + this.fetchAuditLog(true); + } },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/admin/dashboard/static/js/dashboard.js` around lines 79 - 107, The init() routine parses the current route but only triggers fetchAuditLog(true) on popstate, so a direct first-load to the "audit" route shows no data; after parsing the route (using this._parseRoute) and setting this.page, add a call to this.fetchAuditLog(true) when page === 'audit' (similar to how usageMode and renderChart are handled) so initial loads of the audit route fetch data immediately before returning from init().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/admin/dashboard/static/js/modules/audit-list.js`:
- Around line 12-35: fetchAuditLog can be subject to race conditions where a
slower earlier response overwrites a newer one; add a simple in-flight ordering
guard by introducing a monotonically-incremented request token (e.g.
this.auditFetchToken or this._auditRequestId) that you increment at the start of
fetchAuditLog, capture into a local variable, and only apply the response (and
error fallback) to this.auditLog if the captured token still matches the
instance token; implement the check before assigning this.auditLog in both the
success and catch paths so out-of-order responses are ignored.
In `@internal/auditlog/reader_mongodb.go`:
- Around line 198-214: GetLogs is returning row.Data directly from
facetResult.Data which bypasses redaction and can leak sensitive headers; update
the construction of LogEntry in GetLogs to pass row.Data through the existing
redaction helpers (e.g., call sanitizeLogData(row.Data) or use toLogEntry(row)
so the Data field is the sanitized result) before assigning to LogEntry.Data,
ensuring sensitive headers (Authorization, Cookie, X-API-Key, etc.) are removed.
---
Outside diff comments:
In `@internal/admin/dashboard/static/js/dashboard.js`:
- Around line 79-107: The init() routine parses the current route but only
triggers fetchAuditLog(true) on popstate, so a direct first-load to the "audit"
route shows no data; after parsing the route (using this._parseRoute) and
setting this.page, add a call to this.fetchAuditLog(true) when page === 'audit'
(similar to how usageMode and renderChart are handled) so initial loads of the
audit route fetch data immediately before returning from init().
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (12)
cmd/gomodel/docs/docs.gocmd/gomodel/docs/swagger.jsoninternal/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/conversation-drawer.jsinternal/admin/dashboard/templates/index.htmlinternal/auditlog/conversation_helpers.gointernal/auditlog/conversation_helpers_test.gointernal/auditlog/reader_mongodb.gointernal/auditlog/reader_mongodb_test.gointernal/server/handlers.go
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
internal/admin/dashboard/static/js/modules/audit-list.js (1)
26-35:⚠️ Potential issue | 🟠 MajorMove the token guard before
handleFetchResponseto prevent stale auth-state flips.On Line 27,
handleFetchResponsecan setauthError/needsAuthbefore the staleness check on Line 28. A slower older 401 can still overwrite UI auth state after a newer successful request. Also normalizeentriesas an array defensively.Suggested fix
- const res = await fetch('/admin/api/v1/audit/log?' + qs, { headers: this.headers() }); - if (!this.handleFetchResponse(res, 'audit log')) { - if (requestToken !== this.auditFetchToken) return; + const res = await fetch('/admin/api/v1/audit/log?' + qs, { headers: this.headers() }); + if (requestToken !== this.auditFetchToken) return; + if (!this.handleFetchResponse(res, 'audit log')) { this.auditLog = { entries: [], total: 0, limit: 25, offset: 0 }; return; } const payload = await res.json(); if (requestToken !== this.auditFetchToken) return; - this.auditLog = payload; - if (!this.auditLog.entries) this.auditLog.entries = []; + this.auditLog = { + ...payload, + entries: Array.isArray(payload.entries) ? payload.entries : [] + };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/admin/dashboard/static/js/modules/audit-list.js` around lines 26 - 35, Move the staleness guard so you check "if (requestToken !== this.auditFetchToken) return;" immediately after the fetch and before calling this.handleFetchResponse, to avoid an older response changing auth state; keep calling this.handleFetchResponse only for non-stale responses. After parsing payload (const payload = await res.json()), set "if (requestToken !== this.auditFetchToken) return;" again and then assign this.auditLog = payload || {}; and ensure this.auditLog.entries = Array.isArray(this.auditLog.entries) ? this.auditLog.entries : []; so entries is normalized to an array and stale responses never flip authError/needsAuth in handleFetchResponse.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/auditlog/reader_mongodb.go`:
- Around line 215-290: GetConversation currently walks parent/child links with
repeated DB calls (via findByResponseID and findByPreviousResponseID) which is
correct but can be optimized; replace or add an alternative implementation that
uses a single MongoDB aggregation with $graphLookup to fetch the full thread
around the anchor ID (using the same link fields extracted by
extractPreviousResponseID/extractResponseID) up to
clampConversationLimit(limit), then sort and map results into the same
ConversationResult shape, preserving cycle-safety and redaction by reusing
toLogEntry() or existing mapping logic.
- Around line 204-210: The code currently compares mongo.ErrNoDocuments using
`err == mongo.ErrNoDocuments` after calling
r.collection.FindOne(...).Decode(&row); change these to use errors.Is(err,
mongo.ErrNoDocuments) for robust error matching (import the "errors" package if
not already). Update the same pattern wherever similar checks occur (the other
occurrences in the file around the code handling queries at the spots
referenced, e.g., the checks near lines 315 and 331) so all mongo.ErrNoDocuments
comparisons use errors.Is.
---
Duplicate comments:
In `@internal/admin/dashboard/static/js/modules/audit-list.js`:
- Around line 26-35: Move the staleness guard so you check "if (requestToken !==
this.auditFetchToken) return;" immediately after the fetch and before calling
this.handleFetchResponse, to avoid an older response changing auth state; keep
calling this.handleFetchResponse only for non-stale responses. After parsing
payload (const payload = await res.json()), set "if (requestToken !==
this.auditFetchToken) return;" again and then assign this.auditLog = payload ||
{}; and ensure this.auditLog.entries = Array.isArray(this.auditLog.entries) ?
this.auditLog.entries : []; so entries is normalized to an array and stale
responses never flip authError/needsAuth in handleFetchResponse.
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (3)
internal/admin/dashboard/static/js/dashboard.jsinternal/admin/dashboard/static/js/modules/audit-list.jsinternal/auditlog/reader_mongodb.go
| err := r.collection.FindOne(ctx, bson.D{{Key: "_id", Value: id}}).Decode(&row) | ||
| if err != nil { | ||
| if err == mongo.ErrNoDocuments { | ||
| return nil, nil | ||
| } | ||
| return nil, fmt.Errorf("failed to query audit log by id: %w", err) | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Prefer errors.Is() for error comparison.
Direct comparison err == mongo.ErrNoDocuments works, but errors.Is() is more robust and idiomatic Go, especially if the driver behavior changes to wrap errors in future versions. The same pattern appears on lines 315 and 331.
♻️ Proposed refactor
+import "errors"
- if err == mongo.ErrNoDocuments {
+ if errors.Is(err, mongo.ErrNoDocuments) {
return nil, nil
}Apply similar changes to lines 315 and 331.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| err := r.collection.FindOne(ctx, bson.D{{Key: "_id", Value: id}}).Decode(&row) | |
| if err != nil { | |
| if err == mongo.ErrNoDocuments { | |
| return nil, nil | |
| } | |
| return nil, fmt.Errorf("failed to query audit log by id: %w", err) | |
| } | |
| err := r.collection.FindOne(ctx, bson.D{{Key: "_id", Value: id}}).Decode(&row) | |
| if err != nil { | |
| if errors.Is(err, mongo.ErrNoDocuments) { | |
| return nil, nil | |
| } | |
| return nil, fmt.Errorf("failed to query audit log by id: %w", err) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/auditlog/reader_mongodb.go` around lines 204 - 210, The code
currently compares mongo.ErrNoDocuments using `err == mongo.ErrNoDocuments`
after calling r.collection.FindOne(...).Decode(&row); change these to use
errors.Is(err, mongo.ErrNoDocuments) for robust error matching (import the
"errors" package if not already). Update the same pattern wherever similar
checks occur (the other occurrences in the file around the code handling queries
at the spots referenced, e.g., the checks near lines 315 and 331) so all
mongo.ErrNoDocuments comparisons use errors.Is.
| // GetConversation returns a linear conversation thread around a seed log entry. | ||
| func (r *MongoDBReader) GetConversation(ctx context.Context, logID string, limit int) (*ConversationResult, error) { | ||
| limit = clampConversationLimit(limit) | ||
|
|
||
| anchor, err := r.GetLogByID(ctx, logID) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if anchor == nil { | ||
| return &ConversationResult{ | ||
| AnchorID: logID, | ||
| Entries: []LogEntry{}, | ||
| }, nil | ||
| } | ||
|
|
||
| thread := []*LogEntry{anchor} | ||
| seen := map[string]struct{}{anchor.ID: {}} | ||
|
|
||
| current := anchor | ||
| for len(thread) < limit { | ||
| prevID := extractPreviousResponseID(current) | ||
| if prevID == "" { | ||
| break | ||
| } | ||
| parent, err := r.findByResponseID(ctx, prevID) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if parent == nil { | ||
| break | ||
| } | ||
| if _, ok := seen[parent.ID]; ok { | ||
| break | ||
| } | ||
| thread = append([]*LogEntry{parent}, thread...) | ||
| seen[parent.ID] = struct{}{} | ||
| current = parent | ||
| } | ||
|
|
||
| current = anchor | ||
| for len(thread) < limit { | ||
| respID := extractResponseID(current) | ||
| if respID == "" { | ||
| break | ||
| } | ||
| child, err := r.findByPreviousResponseID(ctx, respID) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if child == nil { | ||
| break | ||
| } | ||
| if _, ok := seen[child.ID]; ok { | ||
| break | ||
| } | ||
| thread = append(thread, child) | ||
| seen[child.ID] = struct{}{} | ||
| current = child | ||
| } | ||
|
|
||
| sort.Slice(thread, func(i, j int) bool { | ||
| return thread[i].Timestamp.Before(thread[j].Timestamp) | ||
| }) | ||
|
|
||
| entries := make([]LogEntry, 0, len(thread)) | ||
| for _, entry := range thread { | ||
| if entry != nil { | ||
| entries = append(entries, *entry) | ||
| } | ||
| } | ||
|
|
||
| return &ConversationResult{ | ||
| AnchorID: anchor.ID, | ||
| Entries: entries, | ||
| }, nil | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Implementation is correct with proper cycle detection.
The conversation threading logic correctly:
- Uses a
seenmap to prevent infinite loops from cycles - Traverses both parent and child chains
- Applies redaction through
toLogEntry()in all helper methods
One consideration: this makes sequential database queries (one per entry in the thread), which is bounded by limit but could be optimized with a single aggregation query using $graphLookup if conversation threads become large or latency-sensitive in production.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/auditlog/reader_mongodb.go` around lines 215 - 290, GetConversation
currently walks parent/child links with repeated DB calls (via findByResponseID
and findByPreviousResponseID) which is correct but can be optimized; replace or
add an alternative implementation that uses a single MongoDB aggregation with
$graphLookup to fetch the full thread around the anchor ID (using the same link
fields extracted by extractPreviousResponseID/extractResponseID) up to
clampConversationLimit(limit), then sort and map results into the same
ConversationResult shape, preserving cycle-safety and redaction by reusing
toLogEntry() or existing mapping logic.
Summary by CodeRabbit
New Features
UI Enhancements
Documentation