Skip to content

fix(logs): split summary/detail contracts to make trace tab gate type-safe#4431

Merged
waleedlatif1 merged 14 commits intostagingfrom
waleedlatif1/logs-traces-fix
May 4, 2026
Merged

fix(logs): split summary/detail contracts to make trace tab gate type-safe#4431
waleedlatif1 merged 14 commits intostagingfrom
waleedlatif1/logs-traces-fix

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

@waleedlatif1 waleedlatif1 commented May 4, 2026

Summary

The Trace tab was silently missing from the Log Details sidepanel because list and detail rows shared one WorkflowLog type with executionData: z.unknown(). The UI couldn't distinguish a summary row (no spans) from a detail row (with spans), so the tab gate read undefined and hid itself.

  • Splits into WorkflowLogSummary (list) and WorkflowLogDetail (typed executionData with optional traceSpans) — makes the bug structurally impossible.
  • Detail and new by-execution routes both write through to the same logKeys.detail(id) cache, eliminating two-key fragmentation that caused the merge memo workaround.
  • List route moves to cursor pagination on (sortValue, id) with proper NULLS LAST handling and SQL-side sort across workflow + job execution tables (replaces in-memory merge that dropped rows under filters).
  • Detail route now requires and asserts workspaceId. Deep-link path uses useLogByExecutionId instead of auto-paginating the entire workspace.

Test plan

  • Open Logs page, click any workflow execution → Trace tab appears with correct data
  • Navigate via arrow keys 10× rapidly → no flicker, no missing tabs
  • Right-click a row → preview snapshot → sidebar still shows correct selected log
  • Deep-link ?executionId=<id> → resolves in one round-trip, sidebar opens with detail (even for off-page logs)
  • Sort by cost → server returns sorted results across all pages; Load more works
  • Filter by status=error → page 2 contains correct rows
  • Cancel a running execution → one mutation, invalidates lists() + detail(id), no stats refetch
  • Cross-workspace test: deep-link to ?executionId=<id from other workspace> → 404

…-safe

The Trace tab was silently missing from the Log Details sidepanel because
list and detail rows shared one WorkflowLog type with executionData:
z.unknown(). The UI couldn't distinguish a summary row (no spans) from a
detail row (with spans), so the tab gate read undefined and hid itself.

Splits into WorkflowLogSummary (list) and WorkflowLogDetail (typed
executionData with optional traceSpans). Detail and by-execution routes
both write through to the same logKeys.detail(id) cache, eliminating the
two-key fragmentation that caused the merge memo workaround. List route
moves to cursor pagination on (sortValue, id) with proper NULLS LAST
handling and SQL-side sort across workflow + job execution tables.
Detail route now requires and asserts workspaceId. Deep-link path uses
useLogByExecutionId instead of auto-paginating the entire workspace.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 4, 2026 10:28pm

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 4, 2026

PR Summary

Medium Risk
Moderate risk because it changes the GET /api/logs response shape and pagination semantics (cursor + server-side sorting), adds a new API route, and threads required workspaceId through detail fetches, which can break callers and subtly affect ordering/filtering.

Overview
Fixes log detail rendering by splitting list vs detail types: WorkflowLogSummary for list rows and WorkflowLogDetail with typed executionData (including optional traceSpans) for the sidebar/embedded views, making Trace-tab gating type-safe.

Refactors the logs API surface: GET /api/logs now returns summaries with cursor-based pagination (nextCursor) and server-side sorting by date/duration/cost/status (with NULLS LAST) across workflow + job logs; log detail endpoints now require workspaceId, share a new fetchLogDetail loader, and add GET /api/logs/by-execution/[executionId] for deep-links.

Updates client hooks and UI to match: useLogsList uses cursor paging + sort params, useLogDetail requires workspaceId, deep-linking resolves via useLogByExecutionId, and components switch to contract-driven types while improving Trace-tab/keyboard behavior and cache consistency.

Reviewed by Cursor Bugbot for commit fb0ae86. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 4, 2026

Greptile Summary

This PR fixes the missing Trace tab by splitting WorkflowLog into typed WorkflowLogSummary (list) and WorkflowLogDetail (with executionData) contracts, replaces offset pagination with cursor pagination using (sortValue, id) tiebreakers, and introduces a dedicated by-execution route for single-round-trip deep-link resolution. Arrow-key navigation for deep-linked logs and stats invalidation after cancel/retry are also fixed.

  • The cross-table cursor ID tiebreaker (previously flagged) remains: cursor.id from the merged last row is applied to both workflowExecutionLogs.id and jobExecutionLogs.id, which are independent UUID sequences, silently dropping rows at page boundaries.
  • initializeFromURL() is now called inside a useState lazy initializer, a render-phase side effect that React Strict Mode will double-invoke.

Confidence Score: 3/5

Not safe to merge — the unfixed cross-table cursor tiebreaker causes silent row drops on page 2+ when workflow and job logs are mixed, and the new render-phase side effect risks double-initializing filter state in Strict Mode.

Two P1 findings: the previously flagged cross-table cursor UUID tiebreaker is still present (confirmed not addressed in this diff), and the new useState render-phase side effect for initializeFromURL. Multiple P1s on a core data-fetching path pull the score below the 4/5 P1 ceiling.

apps/sim/app/api/logs/route.ts (cursor tiebreaker), apps/sim/app/workspace/[workspaceId]/logs/logs.tsx (useState side effect)

Important Files Changed

Filename Overview
apps/sim/lib/api/contracts/logs.ts Core contract split: WorkflowLog → WorkflowLogSummary + WorkflowLogDetail; adds cursor-pagination response schema, typed executionData, and new by-execution contract. Well-structured.
apps/sim/app/api/logs/route.ts List route rebuilt with cursor pagination and SQL-side sort; cross-table cursor ID tiebreaker (previously flagged P1) remains unfixed — cursor from one table is applied as tiebreaker to the other table's UUID column.
apps/sim/lib/logs/fetch-log-detail.ts New shared loader for detail routes; queries workflow then job tables with proper permission checks, correctly transforms result to WorkflowLogDetail shape.
apps/sim/hooks/queries/logs.ts Migrated to cursor pagination, split detail/summary types, added workspaceId to useLogDetail; stats and detail invalidations are correctly added back to cancel/retry mutations.
apps/sim/app/workspace/[workspaceId]/logs/logs.tsx Deep-link resolution moved to useLogByExecutionId (cleaner); effectiveSidebarOpen and hasNext correctly guard the deep-link case; uses render-phase side effect in useState initializer (new P1).
apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx showTraceTab gate fixed — now shows optimistically on any execution log and displays loading/empty states; onActiveTabChange wrapped in useCallback; useLayoutEffect used correctly.
apps/sim/app/api/logs/[id]/route.ts Simplified to use shared fetchLogDetail utility; now requires workspaceId for workspace-scoped 404 on cross-workspace access.
apps/sim/app/api/logs/by-execution/[executionId]/route.ts New dedicated route for deep-link resolution; delegates to shared fetchLogDetail with executionId lookup; correct auth and workspace scoping.

Sequence Diagram

sequenceDiagram
    participant UI as Logs UI
    participant ListAPI as GET /api/logs
    participant DetailAPI as GET /api/logs/[id]
    participant ByExecAPI as GET /api/logs/by-execution/[id]
    participant DB as Database

    Note over UI: Normal page load
    UI->>ListAPI: workspaceId + filters + sortBy/sortOrder
    ListAPI->>DB: workflow UNION job (cursor paginated, SQL-sorted)
    DB-->>ListAPI: WorkflowLogSummary[] + nextCursor
    ListAPI-->>UI: { data, nextCursor }

    Note over UI: User clicks a row
    UI->>DetailAPI: logId + workspaceId
    DetailAPI->>DB: fetchLogDetail(id)
    DB-->>DetailAPI: WorkflowLogDetail (with executionData)
    DetailAPI-->>UI: { data: WorkflowLogDetail }
    UI-->>UI: setQueryData(logKeys.detail(id), data)

    Note over UI: Deep-link ?executionId=X
    UI->>ByExecAPI: executionId + workspaceId
    ByExecAPI->>DB: fetchLogDetail(executionId)
    DB-->>ByExecAPI: WorkflowLogDetail
    ByExecAPI-->>UI: { data: WorkflowLogDetail }
    UI-->>UI: setQueryData(logKeys.detail(data.id), data)
    UI-->>UI: dispatch TOGGLE_LOG → sidebar opens
Loading

Comments Outside Diff (1)

  1. apps/sim/app/workspace/[workspaceId]/logs/logs.tsx, line 516-520 (link)

    P1 Render-phase side effect via useState initializer

    initializeFromURL() is called inside a useState lazy initializer, which runs during the render phase. React treats render as a pure operation; side effects here can cause unexpected behaviour. In React Strict Mode, the lazy initializer is invoked twice on mount — initializeFromURL() would parse the URL and write to the Zustand store a second time. If the function is not idempotent (e.g. it appends or merges rather than replaces), this would corrupt the filter state on the first render. Even if it is idempotent today, the pattern is fragile.

    The original useEffect + isInitialized.current guard ran once after mount and is the safe pattern for one-time initialization with side effects.

Reviews (8): Last reviewed commit: "fix(logs): sync active-tab callback befo..." | Re-trigger Greptile

Comment thread apps/sim/app/api/logs/route.ts
Comment thread apps/sim/app/api/logs/[id]/route.ts Outdated
… enhanced spread order

- Move onActiveTabChange call from render into useEffect to avoid
  side-effects during render (StrictMode safety).
- Re-add logKeys.stats() invalidation to cancel/retry mutations so
  the dashboard reflects status flips immediately.
- Reorder enhanced: true after ...execData spread in detail and
  by-execution routes so the literal discriminator is never
  overwritten by stale execData.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The in-memory merge of workflow + job pages negated the comparator
for DESC, which placed null sort values at the start. SQL orders
both ASC and DESC with NULLS LAST, so DESC pages emitted a cursor
{v: <last non-null>, id: ...} while null rows still satisfied the
cursor predicate (OR sort_expr IS NULL) on the next page —
producing duplicate null rows across pages on cost/duration sorts.

Handle nulls explicitly in the JS comparator so they always sort
last regardless of direction, matching the SQL ordering.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
Comment thread apps/sim/app/api/logs/by-execution/[executionId]/route.ts
…invalidation, optimistic detail patch, trace loading state

- Wrap LogDetails -> LogDetailsContent onActiveTabChange in useCallback
  so the child useEffect doesn't refire on every parent render.
- Add logKeys.byExecutionAll() to cancel + retry invalidation so the
  table-embedded sidebar picks up status changes immediately.
- Optimistic write-through to logKeys.detail in useCancelExecution so
  the open sidebar reflects 'cancelling' instantly; rolls back on error.
- Distinguish trace loading from trace-empty: when log.executionData is
  not yet fetched, render "Loading trace…" instead of the empty state.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the legacy `WorkflowLog` / `LogsResponse` / `WorkflowData` /
`CostMetadata` / `ToolCallMetadata` shapes in
`stores/logs/filters/types.ts` with direct use of the contract types
`WorkflowLogSummary`, `WorkflowLogDetail`, and a new `WorkflowLogRow`
alias for surfaces that render either form. Removes the
`summaryToWorkflowLog` / `detailToWorkflowLog` bridge in the React
Query layer along with their double-cast annotations.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Whitelist sort columns against logSortBy enum to prevent client crash
  when non-sortable headers (workflow, trigger) reach the contract parser.
- Extract fetchLogDetail helper shared by /api/logs/[id] and
  /api/logs/by-execution/[executionId] — collapses ~360 duplicated lines
  to a single source of truth keyed on lookup column.
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/logs/route.ts
Comment thread apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
When level=running or level=pending (workflow-only states involving
endedAt/pausedExecutions semantics), jobLevelConditions stayed empty
so no level constraint reached jobConditions — every job log in the
workspace leaked into the result. Skip the job side entirely when the
level filter has no job-applicable values (error/info).
Remove ExecutionLog/RawLogResponse/ExecutionCost/LogWithExecutionData/
TraceSpan/BlockExecution interfaces and the mapToExecutionLog,
mapToExecutionLogAlt, extractOutput functions — all unreferenced after
the contract split. -212 lines.
The previous useEffect-based initializeFromURL caused useLogsList and
useDashboardStats to fire once with default store filters, then refetch
after the effect updated filters from the URL. Move the initial hydrate
into a useState lazy initializer so the first render already reads
URL-derived filters; the popstate handler keeps the existing effect for
back/forward navigation.
waleedlatif1 and others added 2 commits May 4, 2026 12:48
…traces-fix

# Conflicts:
#	apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit e98e80a. Configure here.

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptileai pushing back on both points after re-tracing the code:

P1 — cross-table UUID tiebreaker. Not a bug. The cursor { v, id } is applied to both tables with the same id < cursor.id (DESC) predicate, and the in-memory merge sorts by (v, id) using localeCompare. Because:

  1. Both workflowExecutionLogs.id and jobExecutionLogs.id are canonical lowercase UUID strings — globally lex-comparable.
  2. Postgres uuid-type byte comparison matches JS localeCompare on canonical UUIDs (hex digits + - preserve byte→string ordering).
  3. The SQL filter and the JS merge use identical ordering.

…the predicate (v, id) < cursor is well-defined regardless of which table contributed the cursor row. Concrete trace: workflow W1=(10,'aaa'), job J1=(10,'111'). Page 1 emits W1, cursor=(10,'aaa'). Page 2 SQL on the job table: v=10 AND id < 'aaa' → matches J1 because '111' < 'aaa' (digit < letter in ASCII). No drop. The pattern that genuinely breaks (independent per-source pagination without a unifying boundary, à la combine-pagination's Framed Range Intersection) isn't what this route does — there's a single cursor predicate applied to both tables in one round-trip, then a merge of the bounded results.

P2 — URL hydration double-fetch. Not a bug. useState(() => { useFilterStore.getState().initializeFromURL(); return null }) runs during render, before useFilterStore(useShallow(...)) reads the filters in the same render. Zustand's set() is synchronous, so the selector observes the URL-derived values on the very first render. useLogsList fires once, with the correct filters. No extra request, no defaults flash.

If I'm missing something on either point, happy to dig in further — please point at the specific scenario that drops rows.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 4, 2026

Having read the full route.ts and logs.tsx, both pushbacks hold up.

P1 — Cross-table cursor: Your analysis is correct. buildCursorCondition constructs a composite (sortValue, id) predicate that is applied identically to both tables in the same round-trip. For your W1=(10,'aaa'), J1=(10,'111') example: the condition is (sortExpr = 10 AND id < 'aaa'), and since '111' < 'aaa' holds in both ASCII and JS localeCompare (digit code points 49–57 are below letter code points 97–102), J1 is correctly included on page 2. The inverse case also holds — a job row with id='zzz' would sort before the 'aaa' cursor row in the (10, id DESC) ordering, so it would have appeared on page 1 and the id < 'aaa' filter correctly excludes it from page 2. The global ordering is self-consistent.

P2 — URL hydration: Also correct. The useState initializer is a synchronous call that fires during the render phase, before the useFilterStore(useShallow(...)) call lower in the same function body. Because Zustand's set() is synchronous and mutates the store in-place, the useShallow selector reads the URL-derived state on the very first render. One fetch, correct filters from the start.

I'll withdraw both comments.

Tip: You can give me more context with [.greptile/rules.md](https://www.greptile.com/docs/code-review/custom-standards) so I get this right next time.

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/logs/logs.tsx Outdated
Deep-linked logs resolved via useLogByExecutionId may not be in the
current page list, leaving selectedLogIndex at -1. The hasNext prop
was evaluating -1 < logs.length - 1 (true for any non-empty list),
which enabled the next arrow and jumped to the first item on click.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

…rds aligned

Run the resolvedTab → onActiveTabChange propagation in useLayoutEffect
so the parent's activeTabRef updates synchronously before the next
paint. This closes the brief window where window keydown handlers
in the logs page would still see activeTabRef.current === 'trace'
and short-circuit arrow-key navigation immediately after switching
to a log without a Trace tab.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit fb0ae86. Configure here.

@waleedlatif1 waleedlatif1 merged commit 029ac9f into staging May 4, 2026
14 checks passed
@waleedlatif1 waleedlatif1 deleted the waleedlatif1/logs-traces-fix branch May 4, 2026 23:18
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