Move trace tree building from client to server#317
Conversation
Build trace adjacency maps (TraceTreeEntry) server-side in Haskell and send them as part of the JSON response, replacing the client-side grouping/orphan/virtual-parent logic in JS. This simplifies the frontend considerably and makes trace structure deterministic.
PR Review: Move trace tree building from client to serverSummaryThis PR successfully moves trace tree building logic from JavaScript to Haskell, reducing client-side complexity from ~100 lines to ~20 lines. The server now builds adjacency maps ( Code Quality & Best PracticesHaskell Code (
TypeScript Code (
Potential Bugs & Issues
Performance Considerations
Test Coverage
Security Concerns
Succinctness & GHC ExtensionsCurrent extensions in use: Opportunities for more concise code:
Package UsageGood use of existing packages:
Could leverage more:
Recommendations
Overall AssessmentStrengths:
Concerns:
Verdict: Approve with recommendations. The architectural change is sound, but monitor query performance and consider the succinctness improvements for maintainability. Reviewed with focus on: code quality, potential bugs, performance, test coverage, security, and Haskell succinctness leveraging GHC extensions and ecosystem packages. |
Code Review: PR 317 - Move Trace Tree Building from Client to Server📊 Overall Assessment: 8.5/10This is a high-quality PR that successfully moves trace tree building from client to server, achieving a 52% reduction in client-side code (~130 lines → ~63 lines). ✅ Strengths
|
buildTraceTree was using positional queryResultCount to determine roots, which excluded query result rows from parent-child linking. Now a span is a root only when its parent is absent from the data, and all spans with in-data parents are properly linked as children.
Code Review - PR #317: Move trace tree building from client to serverOverviewThis PR successfully migrates trace tree construction from JavaScript to Haskell, consolidating business logic on the server. The approach is sound and the testing is comprehensive. 🎯 Code Quality & Best PracticesStrengths:
Areas for Improvement: 1. Leverage Available Packages (Lines 265-315 in Log.hs)Several expressions could use functions from -- Current (Line 269):
valText v idx = case v V.\!? idx of Just (AE.String t) | not (T.null t) -> Just t; _ -> Nothing
-- Suggested (using pattern guards more idiomatically):
valText v idx = v V.\!? idx >>= \case AE.String t | not (T.null t) -> Just t; _ -> Nothing
-- Current (Line 296):
sortedChildrenMap = Map.map (sortWith \x -> maybe 0 (.startNs) (Map.lookup x spanMap)) childrenMap
-- Suggested (using `sortOn` from relude):
sortedChildrenMap = Map.map (sortOn (maybe 0 (.startNs) . (spanMap Map.\!?))) childrenMap2. Redundant Null Checks (Lines 191, 194, 313-314)With -- Current:
rootKids = fromMaybe [] (Map.lookup root'.spanId fullChildrenMap)
initAcc = if null rootKids then Map.empty else one (root'.spanId, rootKids)
-- Suggested (more concise):
initAcc = Map.fromList [(root'.spanId, rootKids) | not (null rootKids)]
-- Or even better with catMaybes pattern:
initAcc = maybe Map.empty (one . (root'.spanId,)) (Map.lookup root'.spanId fullChildrenMap)3. Pattern Matching Simplification (Line 305-311)The nested case can be flattened: -- Current:
go (x : xs) minS maxE acc = case Map.lookup x spanMap of
Nothing -> go xs minS maxE acc
Just si -> let ...
-- Suggested:
go (x : xs) minS maxE acc = maybe (go xs minS maxE acc) handleSpan (Map.lookup x spanMap)
where handleSpan si = ...🐛 Potential IssuesQuery Change Concern (Line 88-91 in RequestDumps.hs)The removal of -- Before: WHERE ... and parent_id IS NOT NULL AND id::text \!= ALL(?)
-- After: WHERE ... AND id::text \!= ALL(?)Risk: This may load root spans into Suggestion: Add a docstring explaining why roots are now included in child query results. ⚡ Performance Considerations
🔒 Security✅ No security concerns identified. SQL query uses parameterized queries correctly. 🧪 Test CoverageExcellent coverage with 4 new test cases covering:
Suggestion: Add a test for the duplicate prevention mentioned above. 📉 Code Succinctness OpportunitiesWith GHC extensions enabled ( -- Example from Notify.hs (Line 506-512 in Settings.hs):
-- Current:
("all", Just tid) ->
getTeam tid >>= traverse_ \t -> do
resolveEmails t >>= mapM_ sendTestEmail
forM_ t.slack_channels $ sendSlackAlert alert pid project.title . Just
-- More succinct with ViewPatterns + RecordWildCards:
("all", Just tid) -> getTeam tid >>= traverse_ \t@Team{..} -> do
resolveEmails t >>= mapM_ sendTestEmail
traverse_ (sendSlackAlert alert pid project.title . Just) slack_channelsSimilar opportunities exist in:
📊 Summary
Recommendation: ✅ Approve with minor suggestions The core functionality is solid. The suggestions above would reduce LOC by ~15-20 lines and leverage the project's extensive GHC extensions and package ecosystem more effectively. None are blocking for merge. |
Use query-result-aware root selection: only QR spans whose parent is not also a QR span become roots. This prevents non-matching ancestor spans (e.g. user_add_to_cart when querying method=="GET") from appearing at root level, while preserving full tree nesting when all spans in a trace are query results.
PR Review: Move trace tree building from client to serverSummaryThis PR successfully moves trace tree construction logic from JavaScript to Haskell, reducing client-side code complexity and improving maintainability. The server now builds adjacency maps ( Code Quality & Best PracticesPositives:
Areas for improvement:
Potential Bugs
Performance ConsiderationsPositives:
Concerns:
Test CoverageExcellent doctests:
Missing test cases:
Security ConcernsNo security issues identified. All data flows through existing validation layers. Code Succinctness ImprovementsGiven your priority on concise code and leveraging GHC extensions:
Package UsageThe codebase could leverage these packages more (check if already in dependencies):
Final RecommendationsMerge readiness: ✅ Yes, with minor improvements suggested above Priority fixes before merge:
Nice-to-haves:
Overall: This is a solid refactoring that improves the architecture. The Haskell code is well-tested with doctests, and the simplification on the client side is substantial (~100 lines → ~20 lines). Good work! 🎉 Lines of code: -248 net (434 additions, 682 deletions when accounting for simplified logic) |
Code Review SummaryThis PR successfully moves trace tree building logic from client-side JavaScript to server-side Haskell, simplifying the frontend while centralizing the tree construction. Overall, the implementation is solid with good test coverage. Here are my findings: Haskell Code Quality✅ Strengths
🔍 Improvements for Conciseness1. Reduce redundant patterns (src/Pages/LogExplorer/Log.hs:266-283)-- Current: Verbose case expressions
valText v idx = case v V.!? idx of Just (AE.String t) | not (T.null t) -> Just t; _ -> Nothing
valInt64 v idx = case v V.!? idx of Just (AE.Number n) -> toBoundedInteger n :: Maybe Int64; _ -> Nothing
-- Suggested: Use lens or more functional style
valText v idx = v V.!? idx >>= \case AE.String t | not (T.null t) -> Just t; _ -> Nothing
valInt64 v idx = v V.!? idx >>= preview _Number >>= toBoundedIntegerWith 2. Leverage point-free style (src/Pages/LogExplorer/Log.hs:286-288)-- Current
grouped :: Map.Map Text [SpanInfo]
grouped = Map.fromListWith (<>) $ V.toList $ V.map (\si -> (si.traceIdVal, [si])) spanInfos
-- Suggested: Use foldMap or directly work with vector
grouped = V.foldl' (\acc si -> Map.insertWith (<>) si.traceIdVal [si] acc) Map.empty spanInfos3. Simplify with ViewPatterns (src/Pages/LogExplorer/Log.hs:299-300)-- Current
traceStartTime = viaNonEmpty head $ sort $ mapMaybe (.timestamp) spans
tid = maybe "" (.traceIdVal) (viaNonEmpty head spans)
-- Suggested: Use `sortWith` and avoid intermediate list
traceStartTime = minimumMaybe $ mapMaybe (.timestamp) spans -- from extra package
tid = maybe "" (.traceIdVal) $ listToMaybe spans4. SQL Query (src/Models/Apis/RequestDumps.hs:91)The removal of -- Current: ORDER BY parent_id NULLS LAST, timestamp DESC
-- Question: Does sorting by parent_id first affect query performance?Consider if Minor Code Improvementssrc/Pages/Anomalies.hs:103-112-- Good: Using point-free composition
whenJust systemPromptM
$ div_ [class_ "mb-4"]
. panel_ def{icon = Just "terminal", collapsible = Just False} "System Prompt"
. div_ [class_ "text-xs font-mono whitespace-pre-wrap text-textWeak max-h-96 overflow-y-auto"]
. toHtml✅ Excellent use of function composition to reduce nesting! src/Pages/Bots/Slack.hs:124, 134, 143-- Good: Using whenLeft_
whenLeft_ result (logWelcomeMessageFailure interaction.channel_id)✅ More concise than src/Pages/Settings.hs:510-524-- Good refactoring with composition
("email", Just tid) -> getTeam tid >>= traverse_ (resolveEmails >=> mapM_ sendTestEmail)
("slack", Just tid) -> getTeam tid >>= traverse_ \t -> forM_ t.slack_channels $ sendSlackAlert alert pid project.title . Just✅ Nice use of Kleisli composition and point-free style src/Pkg/EmailTemplates.hs:572-- Current
whenJust ctaM $ uncurry emailButton
-- Consider: Pattern matching in lambda might be clearer
whenJust ctaM \(url, label) -> emailButton url labelTypeScript/JavaScript ChangesPositive Changes
Concerns1. Memory Management (web-components/src/log-list.ts:762)// Capping at 5000 entries - is this sufficient?
this.cachedServerTraces = [...this.cachedServerTraces, ...meta.traces].slice(-5000);
2. Test Coverage GapThe integration tests are good, but missing:
Performance ConsiderationsDatabase Query Change-- Removed: parent_id IS NOT NULL
-- Added: ORDER BY parent_id NULLS LASTConcern: Without
Recommendation: Profile the query with EXPLAIN ANALYZE to ensure performance isn't degraded. Algorithmic ComplexityThe
✅ No obvious performance issues Security✅ No security concerns identified. The changes are purely structural refactoring of internal data representation. Test Coverage✅ Excellent test additions in
Suggestion: Add a performance test for the new server-side tree building with large datasets (10k+ spans). Summary RecommendationsHigh Priority
Medium Priority
Low Priority
Packages to LeverageYou have these in your deps but could use them more:
Final Verdict✅ Approved with minor suggestions This is a solid refactoring that:
The suggestions above are optimizations for code succinctness and leveraging existing packages, not blocking issues. Great work on the doctests and integration tests! 🎉 |
With QR-based root selection, root spans (parent_id=NULL) from selectChildSpansAndLogs are never tree roots and never reachable from QR subtrees. Restoring the filter reduces the result set size.
PR Review: Move trace tree building from client to serverSummaryThis PR successfully moves trace tree construction from the client (JavaScript) to the server (Haskell), reducing client-side complexity from ~100 lines to ~20 lines. The server now builds ✅ Strengths
🎯 Code Quality & Succinctness ImprovementsHigh Priority1. src/Pages/LogExplorer/Log.hs:265-267 - Simplify lookup pattern-- Current (verbose):
lookupIdx = flip HM.lookup colIdxMap
valText v idx = case v V.!? idx of Just (AE.String t) | not (T.null t) -> Just t; _ -> Nothing
valInt64 v idx = case v V.!? idx of Just (AE.Number n) -> toBoundedInteger n :: Maybe Int64; _ -> Nothing
-- Better (using do-notation or lens):
valText v = (v V.!?) >=> \case AE.String t | not (T.null t) -> Just t; _ -> Nothing
valInt64 v = (v V.!?) >=> preview (_Number . _Show) -- if using lens2. src/Pages/LogExplorer/Log.hs:283-285 - Leverage
|
- Update 11 test expected SQL strings to include kind column - Use >=> for valText/valInt64 lookup helpers - Use foldMap for trace grouping
Code Review: PR #317 - Move Trace Tree Building from Client to ServerExecutive Summary✅ Approve with Required Changes Successfully refactors trace tree construction from ~100 lines of JavaScript to server-side Haskell. Good test coverage and clean type design, but has one critical SQL bug and several performance/security improvements needed. 🚨 Critical Issues (Must Fix)1. SQL Query Bug - Missing Root SpansLocation: WHERE ... AND parent_id IS NOT NULLProblem: This filter excludes root spans (parent_id = NULL), breaking trace trees. The PR description says this should be removed but it's still present. Fix: WHERE project_id = ? $dateRangeStr
AND context___trace_id = ANY(?)
AND id::text != ALL(?)
ORDER BY parent_id NULLS LAST, timestamp DESC
LIMIT 20002. Unbounded Recursion RiskLocation: The Fix: go :: [Text] -> Int64 -> Int64 -> Map.Map Text [Text] -> Int -> (Int64, Int64, Map.Map Text [Text])
go _ minS maxE acc depth | depth > 1000 = (minS, maxE, acc) -- Safety limit
go [] minS maxE acc _ = (minS, maxE, acc)
go (x : xs) minS maxE acc depth = -- add depth param, increment on recursion
|
Summary
TraceTreeEntry) server-side in Haskell and include them in the JSON response, replacing the client-side tree grouping/orphan/virtual-parent logicgroupSpansin JS from ~100 lines of tree-building to ~20 lines that just link children from the server-provided adjacency mapparent_id IS NOT NULLfilter and sort byparent_id NULLS LASTTest plan
USE_EXTERNAL_DB=true cabal test integration-tests --test-option="--match=/Log Explorer/"cd web-components && npm test