Skip to content

Move trace tree building from client to server#317

Merged
tonyalaribe merged 9 commits intomasterfrom
server-side-trace-tree
Feb 16, 2026
Merged

Move trace tree building from client to server#317
tonyalaribe merged 9 commits intomasterfrom
server-side-trace-tree

Conversation

@tonyalaribe
Copy link
Contributor

Summary

  • Build trace adjacency maps (TraceTreeEntry) server-side in Haskell and include them in the JSON response, replacing the client-side tree grouping/orphan/virtual-parent logic
  • Simplifies groupSpans in JS from ~100 lines of tree-building to ~20 lines that just link children from the server-provided adjacency map
  • Fixes child span query to remove parent_id IS NOT NULL filter and sort by parent_id NULLS LAST

Test plan

  • Verify log explorer loads and displays trace trees correctly
  • Check expand/collapse on trace entries works
  • Verify pagination (load more) accumulates server traces properly
  • Run integration tests: USE_EXTERNAL_DB=true cabal test integration-tests --test-option="--match=/Log Explorer/"
  • Run web component tests: cd web-components && npm test

tonyalaribe and others added 2 commits February 16, 2026 17:37
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.
@claude
Copy link

claude bot commented Feb 16, 2026

PR Review: Move trace tree building from client to server

Summary

This 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 (TraceTreeEntry) and includes them in JSON responses.


Code Quality & Best Practices

Haskell Code (src/Pages/LogExplorer/Log.hs)

  1. Excellent use of DerivingVia: The TraceTreeEntry type uses DAE.CustomJSON for automatic field name conversion - clean and idiomatic ✅

  2. Doctest included: Good practice with the buildTraceTree doctest, though consider adding more edge cases (empty inputs, orphan spans)

  3. Succinctness opportunities:

    • Line 153: valText v idx = case v V.!? idx of Just (AE.String t) | not (T.null t) -> Just t; _ -> Nothing
      Could use MaybeT or lens combinators for cleaner composition
    • Lines 171-172: grouped = Map.fromListWith (<>) $ V.toList $ V.map (\si -> (si.traceIdVal, [si])) spanInfos
      Consider: grouped = Map.fromListWith (<>) $ V.toList $ (,) <$> (.traceIdVal) <*> pure . pure <*> id $ spanInfos
    • Lines 180-181: The childrenMap construction could leverage mapMaybe instead of list comprehension with Just pid <- [s.parentId]
  4. Function naming: buildTraceEntries and buildEntry are clear but consider mkTraceEntries/mkEntry (more Haskell conventional)

  5. Import cleanup: Line 100 removes unused lookupVecIntByKey - good hygiene ✅

TypeScript Code (web-components/src/log-worker-functions.ts)

  1. Significant simplification: Reduced from complex tree-building logic to simple adjacency linking - excellent ✅

  2. Type safety: Good use of ServerTraceEntry type definition

  3. Removed "virtual parent" concept: This simplification is good but ensure all edge cases are handled server-side


Potential Bugs & Issues

  1. Critical: Child span query change (line 75-78 in RequestDumps.hs):

    -WHERE ... and parent_id IS NOT NULL AND id::text != ALL(?)
    +WHERE ... AND id::text != ALL(?)
    -ORDER BY timestamp DESC LIMIT 2000;
    +ORDER BY parent_id NULLS LAST, timestamp DESC LIMIT 2000;
    • Removing parent_id IS NOT NULL means root spans are now included in child query results
    • This could cause duplicate data if root spans are already in requestVecs
    • The ORDER BY parent_id NULLS LAST suggests roots (NULL parent_id) come last, which might be intentional, but verify this doesn't break pagination or cause performance issues
  2. Data.Scientific import missing bounds check (line 110):

    import Data.Scientific (toBoundedInteger)

    Used in line 154: valInt64 v idx = case v V.!? idx of Just (AE.Number n) -> toBoundedInteger n :: Maybe Int64; _ -> Nothing

    • toBoundedInteger returns Nothing on overflow, which is correct, but large scientific numbers might silently fail
  3. Fallback ID generation (line 159):

    rawId = fromMaybe ("gen-" <> show idx) $ lookupIdx "id" >>= valText row
    • Using show idx creates string IDs like "gen-0", "gen-1" which could collide across different result sets
    • Consider using UUID or trace-scoped IDs
  4. Client-side cache size limit (line 551 in log-list.ts):

    this.cachedServerTraces = [...this.cachedServerTraces, ...meta.traces].slice(-5000);
    • Hard-coded 5000 limit could cause issues if trace trees span more entries
    • Consider using a more sophisticated cache eviction strategy (LRU, by trace age, etc.)
  5. Missing null checks (line 721 in log-worker-functions.ts):

    const rootSpan = spanById.get(entry.root);
    • If entry.root doesn't exist in spanById, trace is filtered out (line 739)
    • This is correct but could silently hide data issues - consider logging

Performance Considerations

  1. Server-side computation: Moving tree building to server is generally good for:

    • Consistent computation (same results for all clients)
    • Reduced client bundle size
    • Better for slow clients
  2. However, consider:

    • Server now computes trees for ALL trace results on every query
    • With LIMIT 2000 child spans + up to 500 query results, this is ~2500 rows per request
    • The buildTraceTree function has O(n log n) complexity due to sorting
    • Multiple nested loops in buildEntry (go function) could be O(n²) in worst case
  3. Child span query performance (line 78):

    ORDER BY parent_id NULLS LAST, timestamp DESC
    • New sort order might not be covered by existing indexes
    • Check if parent_id column has an index, or consider composite index (parent_id, timestamp)
  4. Map operations: Using Map.fromListWith (line 172) is efficient, but the subsequent Map.map (line 182) for sorting children could be expensive with deep trees

  5. TypeScript performance: The client-side code is now much faster - from O(n²) tree building to O(n) map lookup ✅


Test Coverage

  1. Integration tests added: Lines 427-499 in LogSpec.hs add three new test cases:

    • should include traces field with tree structure
    • traces should match request vecs trace IDs
    • children map values reference valid span IDs
  2. Good coverage, but consider adding:

    • Test for orphan spans (spans whose parent isn't in the result set)
    • Test for deeply nested spans (>5 levels)
    • Test for traces with logs (kind=log) mixed with spans
    • Performance test with 2000+ spans
  3. TypeScript tests updated: Lines 840-951 in log-worker.test.ts properly updated to pass serverTraces parameter ✅


Security Concerns

  1. No major security issues detected

  2. SQL injection: The query in RequestDumps.hs uses parameterized queries correctly ✅

  3. Data exposure: The TraceTreeEntry only exposes IDs and metadata, not sensitive span data ✅


Succinctness & GHC Extensions

Current extensions in use: OverloadedRecordDot, DerivingVia, QuasiQuotes, ViewPatterns, etc. are well-utilized.

Opportunities for more concise code:

  1. Use RecordWildCards more (line 156-168):

    mkSpanInfo :: Int -> V.Vector AE.Value -> SpanInfo
    mkSpanInfo idx row =
      let isLog = maybe False (\i -> valText row i == Just "log") (lookupIdx "kind")
          rawId = fromMaybe ("gen-" <> show idx) $ lookupIdx "id" >>= valText row
          ...
       in SpanInfo sid pid tid sns d ts (idx < queryResultCount)

    Could use RecordWildCards and more compact let bindings:

    mkSpanInfo idx row = SpanInfo{..}
      where
        isLog = (== Just "log") . (lookupIdx "kind" >>=) $ valText row
        spanId = fromMaybe ("gen-" <> show idx) $ lookupIdx "id" >>= valText row
        ...
  2. Use maybe instead of case-of (line 191-196):

    go (x : xs) minS maxE acc = case Map.lookup x spanMap of
      Nothing -> go xs minS maxE acc
      Just si -> ...

    Could be:

    go (x : xs) minS maxE acc = maybe
      (go xs minS maxE acc)
      (\si -> ...) 
      (Map.lookup x spanMap)
  3. Leverage lens more - You're already using lens, but could use more traversals/folds to compact the tree walking logic

  4. Consider foldMap for building maps (line 180-181):

    childrenMap = Map.fromListWith (<>) [(pid, [s.spanId]) | s <- spans, Just pid <- [s.parentId], not (Set.member s.spanId queryResultIds)]

    Could use foldMap with Endo or Map.unionsWith

  5. Use containers more efficiently: The Map.fromList + Map.map pattern (lines 172, 182) could be fused with a fold


Package Usage

Good use of existing packages:

  • deriving-aeson for JSON serialization
  • Data.Map.Strict instead of Data.Map
  • Data.Set for membership checks
  • Data.Scientific for numeric conversions

Could leverage more:

  • Consider foldl package for efficient folds if performance becomes an issue
  • The recursion-schemes package could make the tree traversal in buildEntry more elegant
  • unordered-containers (HashMap) might be faster than Map for the spanMap lookup

Recommendations

  1. High Priority:

    • ⚠️ Verify the parent_id IS NOT NULL removal doesn't cause duplicate data or query performance regression
    • 🔍 Add database index on (parent_id, timestamp) if not already present
    • 🧪 Add test cases for edge cases (orphan spans, deep nesting, mixed logs/spans)
  2. Medium Priority:

    • 📝 Improve generated IDs (gen-0, gen-1) to avoid potential collisions
    • 🗑️ Consider more sophisticated client cache eviction than fixed 5000 limit
    • ⚡ Profile buildTraceTree with large datasets (2000+ spans) to ensure performance
  3. Low Priority (Nice-to-have):

    • 🎯 Refactor mkSpanInfo and buildEntry for more conciseness using RecordWildCards and lens
    • 📚 Add more detailed doctests with edge cases
    • 🔄 Consider using recursion-schemes for tree traversal elegance

Overall Assessment

Strengths:

  • ✅ Significant reduction in client-side complexity
  • ✅ Server-side logic is well-tested
  • ✅ Good use of Haskell type system and extensions
  • ✅ Proper separation of concerns

Concerns:

  • ⚠️ SQL query changes need careful verification
  • ⚠️ Performance implications of server-side computation at scale
  • ℹ️ Some opportunities for more concise Haskell code

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.

@claude
Copy link

claude bot commented Feb 16, 2026

Code Review: PR 317 - Move Trace Tree Building from Client to Server

📊 Overall Assessment: 8.5/10

This 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

  1. Excellent architecture decision - offloading tree building to server improves performance
  2. Comprehensive test coverage - both server (Haskell) and client (TypeScript) tests
  3. Good use of Haskell idioms throughout the codebase (using whenLeft_, uncurry, point-free style)
  4. Type safety - proper use of Maybe, Int64, deriving strategies
  5. Great doctests for buildTraceTree function

⚠️ Issues & Recommendations

Priority 1: Fix Before Merge

1. Missing Strictness Annotations

Location: src/Pages/LogExplorer/Log.hs:75-83 and Log.hs:165-173

Add bang patterns to prevent space leaks in data processing:

data SpanInfo = SpanInfo 
  { spanId :: !Text
  , parentId :: !(Maybe Text)
  , traceIdVal :: !Text
  , startNs :: !Int64
  , dur :: !Int64
  , timestamp :: !(Maybe Text)
  , isQueryResult :: !Bool
  }

Same for TraceTreeEntry fields.

2. Inefficient List Append in Recursion

Location: src/Pages/LogExplorer/Log.hs:152

go (kids ++ xs) (min minS si.startNs) (max maxE ce) newAcc

Problem: (kids ++ xs) is O(n) per iteration. For deep trees, this is slow.

Fix: Use Seq for O(1) append:

import Data.Sequence (Seq, (|>), ViewL(..))
import qualified Data.Sequence as Seq

go work minS maxE acc = case Seq.viewl work of
  EmptyL -> (minS, maxE, acc)
  x :< xs -> case Map.lookup x spanMap of
    Nothing -> go xs minS maxE acc
    Just si ->
      let ce = si.startNs + si.dur
          kids = fromMaybe [] (Map.lookup x fullChildrenMap)
          newAcc = if null kids then acc else Map.insert x kids acc
          newWork = xs Seq.>< Seq.fromList kids
       in go newWork (min minS si.startNs) (max maxE ce) newAcc

3. Database Performance - Missing Index

Location: src/Models/Apis/RequestDumps.hs:553

The query now orders by parent_id NULLS LAST, timestamp DESC. Ensure there's an index:

CREATE INDEX idx_parent_id_timestamp ON otel_logs_and_spans(parent_id, timestamp DESC);

Without this index, the query could be slow on large datasets.


Priority 2: Code Succinctness Improvements

4. Use LambdaCase Extension

Location: src/Pages/LogExplorer/Log.hs:146-152

Current:

go (x : xs) minS maxE acc = case Map.lookup x spanMap of
  Nothing -> go xs minS maxE acc
  Just si -> ...

More succinct:

go (x : xs) minS maxE acc = Map.lookup x spanMap & \case
  Nothing -> go xs minS maxE acc  
  Just si -> ...

5. Use HashMap for O(1) Lookups

Location: src/Pages/LogExplorer/Log.hs:137

sortedChildrenMap = Map.map (sortWith \x -> maybe 0 (.startNs) (Map.lookup x spanMap)) childrenMap

This does Map.lookup (O(log n)) for every child. Use HashMap for O(1):

import qualified Data.HashMap.Strict as HM
spanMapHash = HM.fromList [(s.spanId, s) | s <- spans]
-- then use HM.lookup instead of Map.lookup

6. Inefficient List Comprehension

Location: src/Pages/LogExplorer/Log.hs:136

childrenMap = Map.fromListWith (<>) [(pid, [s.spanId]) | s <- spans, not (Set.member s.spanId queryResultIds), Just pid <- [s.parentId]]

Creates many singleton lists then concatenates. Use (++) instead of (<>):

childrenMap = Map.fromListWith (++) [(pid, [s.spanId]) | s <- spans, not (Set.member s.spanId queryResultIds), Just pid <- [s.parentId]]

Or use foldr for better performance on large datasets.


Priority 3: Potential Bugs

7. Empty Trace Handling

Location: src/Pages/LogExplorer/Log.hs:140

tid = maybe "" (.traceIdVal) (viaNonEmpty head spans)

Returns "" if spans is empty, creating a trace with no ID. Better:

buildTraceEntries :: [SpanInfo] -> Maybe TraceTreeEntry
buildTraceEntries [] = Nothing
buildTraceEntries spans@(s:_) = Just $ ...
  where tid = s.traceIdVal

Then: entries = concatMap (maybeToList . buildTraceEntries) (Map.elems grouped)

8. No Input Validation

Location: src/Pages/LogExplorer/Log.hs:64

The queryResultCount parameter has no bounds checking. Could it be negative? Add:

buildTraceEntries queryResultCount spanInfos
  | queryResultCount < 0 = []
  | otherwise = ...

Priority 4: Missing Tests

Good test coverage overall, but missing:

  1. Deep trace trees - could expose stack overflow in recursive go
  2. Circular references in children maps
  3. Int64 overflow scenarios (startNs + dur)
  4. Property-based tests with QuickCheck for tree invariants
  5. Empty/malformed server responses on client side

🎯 Client-Side Improvements

The TypeScript simplification is excellent:

  • ✅ Removed ~100 lines of complex tree-building logic
  • ✅ Removed virtual parent handling
  • ✅ Removed orphan detection
  • ✅ Cleaner type definitions
  • ✅ Better maintainability

📝 Other Good Changes in This PR

Nice refactoring improvements throughout:

  • Using whenLeft_ instead of verbose either patterns
  • Using uncurry for cleaner code (src/Pkg/EmailTemplates.hs:517)
  • Point-free style improvements (forM_ headers $ th_ . toHtml)
  • Better operator precedence handling

These show excellent Haskell idiom knowledge! 👍


🔒 Security

  • ✅ Parameterized SQL queries (using ? placeholders)
  • ✅ No unsafe operations
  • ⚠️ No limit on number of traces returned - could be DoS vector (consider pagination)

📌 Summary

Approve with recommendations. The core functionality is solid, tests are comprehensive, and the architecture improvement is valuable. The suggested optimizations (strictness, Seq, HashMap) will improve performance and prevent potential issues at scale.

The Priority 1 items (strictness annotations, Seq optimization, DB index) should be addressed before merging for production robustness.

Great work on this refactoring! 🚀

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.
@claude
Copy link

claude bot commented Feb 16, 2026

Code Review - PR #317: Move trace tree building from client to server

Overview

This 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 Practices

Strengths:

  • Clean separation of concerns with TraceTreeEntry type
  • Excellent doctest coverage in buildTraceTree function (Lines 206-264)
  • Proper use of deriving-aeson for JSON serialization
  • Good use of strict Map for efficient lookups

Areas for Improvement:

1. Leverage Available Packages (Lines 265-315 in Log.hs)

Several expressions could use functions from relude or other dependencies:

-- 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.\!?))) childrenMap

2. Redundant Null Checks (Lines 191, 194, 313-314)

With fromMaybe [], the subsequent null checks are redundant:

-- 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 Issues

Query Change Concern (Line 88-91 in RequestDumps.hs)

The removal of parent_id IS NOT NULL filter changes query semantics:

-- 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 childSpansList when they're already in requestVecs, causing duplicates. The integration tests should catch this, but verify the alreadyLoadedIds exclusion works correctly for roots.

Suggestion: Add a docstring explaining why roots are now included in child query results.


⚡ Performance Considerations

  1. Map Traversals (Line 294-296): Building childrenMap uses a list comprehension. Consider using Map.fromListWith directly with a fold over spans for better performance.

  2. Vector Operations (Line 287): V.imap mkSpanInfo followed by V.toList - consider if you need the intermediate vector or can work with lists throughout.

  3. Caching Strategy (Line 733-738 in log-list.ts): The 5000-entry cap is reasonable, but consider if trace entries should be indexed by trace_id for O(1) lookup during pagination.


🔒 Security

✅ No security concerns identified. SQL query uses parameterized queries correctly.


🧪 Test Coverage

Excellent coverage with 4 new test cases covering:

  • Basic tree structure
  • Trace ID matching
  • Children map validation
  • Edge cases (orphans, nested spans, mixed logs/spans)

Suggestion: Add a test for the duplicate prevention mentioned above.


📉 Code Succinctness Opportunities

With GHC extensions enabled (LambdaCase, RecordWildCards, ViewPatterns), several patterns can be more concise:

-- 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_channels

Similar opportunities exist in:

  • Pages/Projects.hs Lines 393, 410-411
  • Pages/Settings.hs Lines 507-525
  • Pkg/EmailTemplates.hs Lines 557, 565-571

📊 Summary

Aspect Rating Notes
Architecture ⭐⭐⭐⭐⭐ Excellent migration to server-side
Code Quality ⭐⭐⭐⭐ Good, with room for terseness
Testing ⭐⭐⭐⭐⭐ Comprehensive coverage
Performance ⭐⭐⭐⭐ Good, minor optimization opportunities
Security ⭐⭐⭐⭐⭐ No concerns

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.
@claude
Copy link

claude bot commented Feb 16, 2026

PR Review: Move trace tree building from client to server

Summary

This 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 (TraceTreeEntry) that the client simply links together.


Code Quality & Best Practices

Positives:

  • ✅ Well-documented with comprehensive doctests covering edge cases (orphans, deep nesting, mixed logs/spans)
  • ✅ Good use of viaNonEmpty head for safe list operations
  • ✅ Proper type safety with TraceTreeEntry data type using Deriving.Aeson for JSON serialization
  • ✅ Consistent formatting improvements (import ordering, alignment)

Areas for improvement:

  1. src/Pages/LogExplorer/Log.hs:268-270 - Verbose lambda can be simplified:

    -- Current:
    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
    
    -- Suggested:
    lookupIdx = (`HM.lookup` colIdxMap)
    valText v = (v V.!?) >=> \case AE.String t | not (T.null t) -> Just t; _ -> Nothing
    valInt64 v = (v V.!?) >=> \case AE.Number n -> toBoundedInteger n; _ -> Nothing
  2. src/Pages/LogExplorer/Log.hs:176 - List comprehension can use mapMaybe:

    -- Current:
    childrenMap = Map.fromListWith (<>) [(pid, [s.spanId]) | s <- spans, Just pid <- [s.parentId], Map.member pid spanMap]
    
    -- Suggested:
    childrenMap = Map.fromListWith (<>) $ mapMaybe (\s -> (,[s.spanId]) <$> s.parentId) $ filter ((`Map.member` spanMap) . fromMaybe "" . (.parentId)) spans
  3. src/Pages/LogExplorer/Log.hs:195 - Redundant list construction:

    -- Current:
    initAcc = Map.fromList [(root'.spanId, rootKids) | not (null rootKids)]
    
    -- Suggested:
    initAcc = Map.fromList $ (root'.spanId, rootKids) <$ guard (not $ null rootKids)
  4. Multiple files - The pattern whenJust x $ \y -> ... appears frequently and could use point-free style or fmap:

    -- src/Pkg/EmailTemplates.hs:560
    whenJust ctaM $ uncurry emailButton  -- ✅ Good!
    
    -- src/Pages/Settings.hs:509 could be:
    getTeam tid >>= traverse_ (traverse_ sendTestEmail . resolveEmails)
  5. src/Pages/Bots/Slack.hs:124, 134, 143, 152 - Excellent use of whenLeft_ replacing either (\err -> ...) (const pass). This is exactly the kind of succinctness we want!


Potential Bugs

  1. src/Pages/LogExplorer/Log.hs:340 - Filtering empty trace IDs might hide issues:

    traceIds = V.filter (not . T.null) $ V.catMaybes $ V.map (\v -> lookupVecTextByKey v colIdxMap "trace_id") requestVecs

    If trace_ids are genuinely empty, this silently drops them. Consider logging or investigating why they'd be empty.

  2. web-components/src/log-list.ts:735-740 - Cache size limit of 5000:

    this.cachedServerTraces = [...this.cachedServerTraces, ...meta.traces].slice(-5000);

    This arbitrary cap could cause inconsistencies if a user paginates beyond 5000 traces. Consider either:

    • Using a proper LRU cache
    • Warning when approaching the limit
    • Keying by trace_id to deduplicate

Performance Considerations

Positives:

  • ✅ Moving tree building to server reduces client-side processing
  • ✅ Adjacency map representation is efficient for tree traversal
  • ✅ Test coverage shows handling of 2540 items (254 traces × 10 spans)

Concerns:

  1. Memory usage - buildTraceTree constructs multiple Maps (spanMap, childrenMap, sortedChildrenMap). For large result sets, this could be memory-intensive. Consider streaming or chunking for very large traces.

  2. Database query change (src/Models/Apis/RequestDumps.hs:90-91):

    -- Removed: parent_id IS NOT NULL
    -- Added: ORDER BY parent_id NULLS LAST

    This now fetches ALL spans/logs including roots. The NULLS LAST ordering may not be optimal for parent-child linking. Consider adding an index on (parent_id, timestamp DESC) if not present.


Test Coverage

Excellent doctests:

  • ✅ Single root, parent-child relationships
  • ✅ Orphan exclusion (missing parents)
  • ✅ Deep nesting (>5 levels)
  • ✅ Mixed logs and spans
  • ✅ Integration tests verify JSON response structure

Missing test cases:

  • ⚠️ Multiple traces with overlapping span IDs (cross-trace collision)
  • ⚠️ Trace with >10,000 spans (stress test)
  • ⚠️ Malformed data (null timestamps, negative durations)

Security Concerns

No security issues identified. All data flows through existing validation layers.


Code Succinctness Improvements

Given your priority on concise code and leveraging GHC extensions:

  1. Use LambdaCase extension (appears to be enabled based on codebase style):

    -- src/Pages/LogExplorer/Log.hs:187
    go (x : xs) minS maxE acc = case Map.lookup x spanMap of
    
    -- Could be:
    go (x : xs) minS maxE acc = Map.lookup x spanMap & \case
  2. Use OverloadedRecordDot more - Already used well throughout (.spanId, .children), but could extend to more places.

  3. Leverage foldMap and Foldable:

    -- src/Pages/LogExplorer/Log.hs:168
    grouped = Map.fromListWith (<>) $ V.toList $ V.map (\si -> (si.traceIdVal, [si])) spanInfos
    
    -- Could be:
    grouped = foldMap (\si -> Map.singleton si.traceIdVal [si]) spanInfos
  4. Use sortOn instead of sortWith where applicable (src/Pages/LogExplorer/Log.hs:177, src/Pages/Replay.hs:474):

    sortedChildrenMap = Map.map (sortOn \x -> maybe 0 (.startNs) (Map.lookup x spanMap)) childrenMap

Package Usage

The codebase could leverage these packages more (check if already in dependencies):

  • vector-algorithms - for in-place sorting of Vectors (more efficient than V.fromList . sort . V.toList)
  • lens - Already imported but underutilized; could simplify nested Map/Vector operations
  • generic-lens - For generic record access if dealing with many similar record types

Final Recommendations

Merge readiness: ✅ Yes, with minor improvements suggested above

Priority fixes before merge:

  1. None blocking

Nice-to-haves:

  1. Simplify lambdas and use point-free style where clearer
  2. Add database index for new query pattern
  3. Document the 5000-trace cache limit in TypeScript

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)

@claude
Copy link

claude bot commented Feb 16, 2026

Code Review Summary

This 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

  • Excellent doctests: buildTraceTree has comprehensive doctests covering edge cases (orphans, deep nesting, mixed logs/spans)
  • Type safety: New TraceTreeEntry type with proper Aeson derivation via deriving-aeson
  • Algorithm clarity: The trace building logic is well-structured with clear separation of concerns

🔍 Improvements for Conciseness

1. 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 >>= toBoundedInteger

With lens-aeson already in dependencies, _Number and _String prisms would be more succinct.

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 spanInfos

3. 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 spans

4. SQL Query (src/Models/Apis/RequestDumps.hs:91)

The removal of parent_id IS NOT NULL is correct, but the sort change needs verification:

-- Current: ORDER BY parent_id NULLS LAST, timestamp DESC
-- Question: Does sorting by parent_id first affect query performance?

Consider if ORDER BY timestamp DESC alone suffices, or add an index on (parent_id, timestamp).


Minor Code Improvements

src/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 either (\err -> ...) (const pass)

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 label

TypeScript/JavaScript Changes

Positive Changes

  • Removed ~70 lines of complex client-side tree building logic
  • Simplified groupSpans from ~100 lines to ~20 lines
  • Server-driven architecture: Client now just links pre-built adjacency map

Concerns

1. Memory Management (web-components/src/log-list.ts:762)

// Capping at 5000 entries - is this sufficient?
this.cachedServerTraces = [...this.cachedServerTraces, ...meta.traces].slice(-5000);
  • For heavy pagination, 5000 traces could still be substantial memory
  • Consider: Clearing cache on query changes or implementing LRU eviction

2. Test Coverage Gap

The integration tests are good, but missing:

  • Test for the 5000-entry cache limit behavior
  • Test for what happens when serverTraces is stale after expand/collapse

Performance Considerations

Database Query Change

-- Removed: parent_id IS NOT NULL
-- Added: ORDER BY parent_id NULLS LAST

Concern: Without parent_id IS NOT NULL, you're now fetching root spans (parent_id = NULL) that you may not need. This could:

  1. Increase result set size
  2. Require post-filtering in Haskell

Recommendation: Profile the query with EXPLAIN ANALYZE to ensure performance isn't degraded.

Algorithmic Complexity

The buildTraceTree function has good complexity:

  • O(n) for building span maps
  • O(n) for building children adjacency
  • O(n) for subtree traversal

✅ 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 test/integration/Pages/LogExplorer/LogSpec.hs:

  • Trace structure validation
  • Children map validation
  • Empty/missing data handling

Suggestion: Add a performance test for the new server-side tree building with large datasets (10k+ spans).


Summary Recommendations

High Priority

  1. Verify SQL performance: Test the impact of removing parent_id IS NOT NULL filter
  2. Consider lens-aeson: Use _Number, _String prisms for more concise parsing

Medium Priority

  1. Memory: Monitor the 5000-entry cache limit in production
  2. Simplify helpers: Use sortWith, minimumMaybe from extra package (already in deps)

Low Priority

  1. Point-free style: Apply more consistently where it improves readability
  2. Documentation: Add a comment explaining why orphan spans (no parent in result set) are excluded

Packages to Leverage

You have these in your deps but could use them more:

  • extra: Has minimumMaybe, maximumMaybe, fromMaybe, etc.
  • lens-aeson: Prisms for JSON values (_String, _Number, _Bool)
  • safe: Has headMay, maximumMay for safer list operations

Final Verdict

Approved with minor suggestions

This is a solid refactoring that:

  • Reduces client-side complexity significantly
  • Centralizes tree logic in Haskell where it's easier to test and maintain
  • Has comprehensive test coverage
  • Follows Haskell best practices

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.
@claude
Copy link

claude bot commented Feb 16, 2026

PR Review: Move trace tree building from client to server

Summary

This 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 TraceTreeEntry adjacency maps that the client simply renders.


✅ Strengths

  1. Excellent architectural improvement - Moving tree building to the server is the right choice for performance and maintainability
  2. Comprehensive doctests - The buildTraceTree function has 4 detailed doctest examples covering edge cases
  3. Good refactoring - Simplified client code significantly while maintaining functionality
  4. Well-tested - Added 3 new integration tests covering trace structure validation

🎯 Code Quality & Succinctness Improvements

High Priority

1. 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 lens

2. src/Pages/LogExplorer/Log.hs:283-285 - Leverage foldMap from Relude

-- Current:
grouped :: Map.Map Text [SpanInfo]
grouped = Map.fromListWith (<>) $ V.toList $ V.map (\si -> (si.traceIdVal, [si])) spanInfos

-- Better (more idiomatic):
grouped = Map.fromListWith (<>) $ V.toList $ fmap (\si -> (si.traceIdVal, [si])) spanInfos
-- Or even better with foldMap:
grouped = foldMap (\si -> Map.singleton si.traceIdVal [si]) spanInfos

3. src/Pages/LogExplorer/Log.hs:292 - Simplify filter with guard

-- Current:
childrenMap = Map.fromListWith (<>) [(pid, [s.spanId]) | s <- spans, Just pid <- [s.parentId], Map.member pid spanMap]

-- Better (clearer intent):
childrenMap = Map.fromListWith (<>) 
  [ (pid, [s.spanId]) 
  | s <- spans
  , Just pid <- [s.parentId]
  , pid `Map.member` spanMap 
  ]
-- Could also use 'mapMaybe' for more functional style

4. src/Pages/Anomalies.hs:101-110 - Redundant lambda with uncurry

-- Current:
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

-- This is good! The change from lambda to point-free with composition is exactly the right direction.

5. src/Pages/Settings.hs:507-512 - Lambda eta-reduction opportunities

-- Current:
("slack", Just tid) -> getTeam tid >>= traverse_ \t -> forM_ t.slack_channels $ sendSlackAlert alert pid project.title . Just

-- Better (remove lambda, use traverse with composition):
("slack", Just tid) -> getTeam tid >>= traverse_ (traverse_ (sendSlackAlert alert pid project.title . Just) . (.slack_channels))

Medium Priority

6. Unused import cleanup - src/Pages/LogExplorer/Log.hs:172

Removed lookupVecIntByKey but it might be useful elsewhere. Verify it's not needed in other modules.

7. Magic numbers - web-components/src/log-list.ts:760

this.cachedServerTraces = [...this.cachedServerTraces, ...meta.traces].slice(-5000);

Extract 5000 to a named constant: const MAX_CACHED_TRACES = 5000;

8. Type annotation in tests - test/integration/Pages/LogExplorer/LogSpec.hs:677

let vecTraceIds = V.toList $ V.mapMaybe (\v -> case v V.!? idx of Just (AE.String t) -> Just t; _ -> Nothing) requestVecs
-- Could use helper function to reduce repetition across tests

🔒 Security & Correctness

✅ Good

  • SQL query change (line 88-90) removes redundant filter - correct
  • Proper handling of orphaned spans (only include children whose parents exist)
  • Timeout handling preserved in notification code

⚠️ Minor Concerns

  1. Unbounded growth risk mitigated - The 5000-entry cap on cachedServerTraces is good, but consider also clearing cache on query changes (not just pagination)
  2. Empty trace handling - Line 372: traces field should handle empty list gracefully (which it does via || [])

🚀 Performance Considerations

✅ Improvements

  • Moving tree building to server reduces client memory pressure
  • Server-side adjacency map is more efficient than client-side tree walking
  • Removed virtual parent node creation (which was O(n) overhead)

💡 Potential Optimizations

  1. buildTraceTree (line 263): The go function traverses children recursively. Consider using a Map.restrictKeys to prune fullChildrenMap early
  2. sortWith usage: Line 264 sortWith (Down . (.startTime)) - good! Much better than sortBy (comparing (Down . (.startTime)))

📝 Test Coverage

Good:

  • 3 new integration tests validate trace structure
  • Doctests cover edge cases (orphans, deep nesting, logs vs spans)

Could improve:

  • Test case for very large trace trees (100+ spans) to verify performance
  • Test pagination with trace accumulation

🎨 Code Style

Excellent Changes

  1. src/Pages/Bots/Slack.hs:123 - whenLeft_ usage instead of either (f) (const pass) - perfect!
  2. src/Pkg/EmailTemplates.hs:571 - uncurry emailButton instead of lambda - exactly right
  3. src/Pages/Projects.hs:394 - Removing fromMaybe [] . fmap in favor of maybe [] - good simplification
  4. src/Pages/Replay.hs:454 - Using whenRight helper instead of case - nice!

Minor Style Notes

  • Import ordering is alphabetical within groups (good)
  • Some long lines in doctests (acceptable for documentation)

🐛 Potential Issues

Low severity:

  1. Line 89: Changed query removes parent_id IS NOT NULL filter. Verify this doesn't cause performance regression on large datasets with many root spans
  2. web-components/src/log-worker-functions.ts:825: Empty serverTraces array returns empty tree (line 852). Document this behavior in JSDoc

📊 Metrics

  • Lines removed: 248
  • Lines added: 435
  • Net change: +187 (acceptable for feature migration)
  • Client LOC reduction: ~80 lines (excellent!)
  • Server LOC increase: ~140 lines (complex logic moved to strongly-typed backend)

🎯 Recommendations Summary

Must address:

  1. Extract magic number 5000 to constant
  2. Consider using foldMap for Map construction (more idiomatic Haskell)

Should consider:
3. Add docstrings to TraceTreeEntry type explaining the adjacency map structure
4. Performance test with 1000+ span traces
5. Use more point-free style where it improves clarity (as done in some places already)

Nice to have:
6. Helper functions for common vector lookup patterns in tests
7. Consider lens for JSON field access (if not avoiding lens)


✅ Verdict

Approve with minor suggestions. This is a solid refactoring that achieves its goals. The code quality is high, tests are comprehensive, and the architectural improvement is clear. The Haskell code leverages available extensions well (OverloadedRecordDot, LambdaCase, etc.).

Score: 8.5/10

  • Code quality: ✅
  • Testing: ✅
  • Performance: ✅
  • Succinctness: 7/10 (some opportunities remain)
  • Security: ✅

Great work! The migration to server-side tree building is a smart move that will make future enhancements easier.

- Update 11 test expected SQL strings to include kind column
- Use >=> for valText/valInt64 lookup helpers
- Use foldMap for trace grouping
@tonyalaribe tonyalaribe merged commit 8df1a43 into master Feb 16, 2026
7 checks passed
@tonyalaribe tonyalaribe deleted the server-side-trace-tree branch February 16, 2026 19:35
@github-actions github-actions bot restored the server-side-trace-tree branch February 16, 2026 19:36
@claude
Copy link

claude bot commented Feb 16, 2026

Code Review: PR #317 - Move Trace Tree Building from Client to Server

Executive 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 Spans

Location: src/Models/Apis/RequestDumps.hs:552

WHERE ... AND parent_id IS NOT NULL

Problem: 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 2000

2. Unbounded Recursion Risk

Location: src/Pages/LogExplorer/Log.hs:186

The go function lacks depth limits, risking stack overflow on deep/malicious trees.

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

⚠️ High Priority

3. Missing Strictness Annotations

Location: src/Pages/LogExplorer/Log.hs:71-80

Add bang patterns for performance (especially on large trace sets):

data TraceTreeEntry = TraceTreeEntry
  { traceId :: !Text
  , startTime :: !Int64
  , duration :: !Int64
  , traceStartTime :: !(Maybe Text)
  , root :: !Text
  , children :: !(Map.Map Text [Text])
  }

4. Inefficient List Concatenation - O(n²)

Location: src/Pages/LogExplorer/Log.hs:192

go (kids ++ xs) -- O(n) per iteration

Fix with Data.Sequence:

import Data.Sequence (Seq, (|>), ViewL(..), viewl)

go :: Seq Text -> Int64 -> Int64 -> Map.Map Text [Text] -> (Int64, Int64, Map.Map Text [Text])
go queue minS maxE acc = case viewl queue of
  EmptyL -> (minS, maxE, acc)
  x :< xs -> case Map.lookup x spanMap of
    Nothing -> go xs minS maxE acc
    Just si ->
      let kids = fromMaybe [] (Map.lookup x fullChildrenMap)
          newQueue = foldl' (|>) xs kids  -- O(1) append
       in go newQueue (min minS si.startNs) (max maxE ce) newAcc

5. SQL Injection Risk

Location: src/Models/Apis/RequestDumps.hs:550-554

String interpolation for dateRangeStr mixes with SQL. Use parameterized queries:

let (dateRangeQ, dateRangeParams) = case (fromTime, toTime) of
      (Just a, Just b) -> ("AND timestamp BETWEEN ? AND ?", [toField a, toField b])
      _ -> ("", [])
results <- PG.query (Query $ encodeUtf8 q) (pid : dateRangeParams ++ [traceIds, excludedSpanIds])

💡 Code Succinctness Improvements

Given the project's priority on concise code and extensive GHC extensions:

6. Verbose Pattern Matching

Location: src/Pages/LogExplorer/Log.hs:149-163

Use RecordWildCards more effectively:

mkSpanInfo idx row = SpanInfo{..}
  where
    isLog = (== Just "log") $ valText row =<< lookupIdx "kind"
    spanId = if isLog then rawId else fromMaybe rawId rawLb
    parentId = if isLog then rawLb else valText row =<< lookupIdx "parent_span_id"
    -- ... rest with direct field names

7. Redundant Singleton Lists

Location: src/Pages/LogExplorer/Log.hs:176

-- Current
childrenMap = Map.fromListWith (<>) [(pid, [s.spanId]) | s <- spans, Just pid <- [s.parentId], ...]

-- Better
childrenMap = Map.fromListWith (<>) $ mapMaybe (\s -> (,[s.spanId]) <$> s.parentId) 
  $ filter (maybe False (`Map.member` spanMap) . parentId) spans

8. Use HashMap for O(1) Lookups

Location: src/Pages/LogExplorer/Log.hs:172

-- Change from Map (O(log n)) to HashMap (O(1))
import qualified Data.HashMap.Strict as HM

grouped :: HM.HashMap Text [SpanInfo]
grouped = V.foldl' (\m si -> HM.insertWith (<>) si.traceIdVal [si] m) HM.empty spanInfos

🧪 Test Coverage

Strengths

  • ✅ Excellent doctests covering edge cases (orphans, deep nesting, mixed logs/spans)
  • ✅ Comprehensive integration tests in LogSpec.hs
  • ✅ Good stress test (254 traces × 10 spans) in log-worker.test.ts

Missing

  • Property tests for tree invariants (no cycles, all roots are query results)
  • Error case coverage (negative timestamps, circular refs)
  • Performance benchmarks for large trace sets

🔍 Additional Observations

Performance

  • Query LIMIT 2000 is arbitrary - what if trace has >2000 spans?
  • Consider adding ORDER BY to selectChildSpansAndLogs for index utilization
  • Vector operations could use vector-algorithms for sorting large child lists

Observability

Given this is an observability platform, consider instrumenting the new code:

buildTraceTree colIdxMap qrCount rows = Tracing.inSpan "buildTraceTree" do
  Tracing.addAttribute "row_count" (V.length rows)
  -- ... existing logic

Documentation

Add module-level Haddock explaining the refactoring rationale and algorithm.


✅ What's Good

  1. Excellent type safety with TraceTreeEntry and proper JSON derivation
  2. Clean separation of concerns with SpanInfo internal type
  3. Comprehensive documentation in doctests
  4. Significant complexity reduction on client side (~100 → ~20 lines)
  5. Good test coverage across integration and unit tests

📋 Action Items

Before Merge:

  • Fix SQL query - remove parent_id IS NOT NULL filter
  • Add recursion depth limit to go function
  • Add strictness annotations to TraceTreeEntry

Recommended:

  • Fix list concatenation performance (use Sequence)
  • Use parameterized SQL queries
  • Add property tests
  • Consider HashMap for better perf on large trace sets

Great refactoring overall! The server-side approach is the right direction. Just need to address the SQL bug before merging. 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant