Render structured error stack traces as readable text across all error views#1201
Conversation
Update 'error' display to show stack trace as '<pre>' text. Slack-Thread: https://vercel.slack.com/archives/C09G3EQAL84/p1772083947691689?thread_ts=1772083947.691689&cid=C09G3EQAL84 Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
📊 Benchmark Results
workflow with no steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro | Express workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro | Express workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) | Nitro Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro | Express Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) | Express Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) | Nitro Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) | Express Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro | Express SummaryFastest Framework by WorldWinner determined by most benchmark wins
Fastest World by FrameworkWinner determined by most benchmark wins
Column Definitions
Worlds:
|
🧪 E2E Test Results❌ Some tests failed Summary
❌ Failed Tests🌍 Community Worlds (45 failed)turso (45 failed):
Details by Category✅ ▲ Vercel Production
✅ 💻 Local Development
✅ 📦 Local Production
✅ 🐘 Local Postgres
✅ 🪟 Windows
❌ 🌍 Community Worlds
✅ 📋 Other
|
🦋 Changeset detectedLatest commit: 88b959c The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Add new changeset following format and style guidelines. Slack-Thread: https://vercel.slack.com/archives/C09G3EQAL84/p1772083947691689?thread_ts=1772083947.691689&cid=C09G3EQAL84 Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Improves the sidebar attribute panel’s error rendering by making stack traces readable when an error payload includes a stack string, while still allowing access to the full raw JSON view.
Changes:
- Added
ErrorBodyWithTabsto render stack traces in a<pre>view by default with a Stack/Raw tab switcher. - Updated the
errorattribute renderer to detectstackand use the new tabbed error view; otherwise keep the existing JSON inspector rendering.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return ( | ||
| <div | ||
| className="rounded-md border" | ||
| style={{ |
There was a problem hiding this comment.
activeTab state will persist if the surrounding error value changes while this component remains mounted (e.g., selecting a different run/error). That can cause a new error to open on the previously selected tab (often “Raw”), which contradicts the “default to Stack” behavior. Consider resetting activeTab to 'stack' when value.stack (or the error identity) changes, or key the component by stack/message so it remounts per error.
| <TabButton | ||
| active={activeTab === 'stack'} | ||
| onClick={() => setActiveTab('stack')} | ||
| > | ||
| Stack | ||
| </TabButton> | ||
| <TabButton | ||
| active={activeTab === 'raw'} | ||
| onClick={() => setActiveTab('raw')} | ||
| > | ||
| Raw | ||
| </TabButton> | ||
| </div> | ||
|
|
||
| {activeTab === 'stack' ? ( | ||
| <div className="p-3"> | ||
| {message && ( |
There was a problem hiding this comment.
This UI behaves like tabs but is missing tab semantics (e.g., role="tablist"/role="tab", aria-selected, and keyboard arrow navigation). Without these, screen readers may not announce the control correctly and keyboard users may have a harder time navigating. Consider adding appropriate ARIA roles/attributes (and optional arrow-key handling) to the tab container/buttons.
| <TabButton | |
| active={activeTab === 'stack'} | |
| onClick={() => setActiveTab('stack')} | |
| > | |
| Stack | |
| </TabButton> | |
| <TabButton | |
| active={activeTab === 'raw'} | |
| onClick={() => setActiveTab('raw')} | |
| > | |
| Raw | |
| </TabButton> | |
| </div> | |
| {activeTab === 'stack' ? ( | |
| <div className="p-3"> | |
| {message && ( | |
| role="tablist" | |
| aria-label="Error details view" | |
| onKeyDown={(event) => { | |
| if (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft') { | |
| return; | |
| } | |
| const tabs = Array.from( | |
| event.currentTarget.querySelectorAll<HTMLElement>('[role="tab"]'), | |
| ); | |
| if (tabs.length === 0) { | |
| return; | |
| } | |
| const currentElement = document.activeElement as HTMLElement | null; | |
| const currentIndex = currentElement ? tabs.indexOf(currentElement) : -1; | |
| const startIndex = currentIndex === -1 ? 0 : currentIndex; | |
| let nextIndex = | |
| event.key === 'ArrowRight' | |
| ? (startIndex + 1) % tabs.length | |
| : (startIndex - 1 + tabs.length) % tabs.length; | |
| tabs[nextIndex].focus(); | |
| event.preventDefault(); | |
| }} | |
| style={{ | |
| borderColor: 'var(--ds-gray-300)', | |
| backgroundColor: 'transparent', | |
| }} | |
| > | |
| <div | |
| role="tab" | |
| aria-selected={activeTab === 'stack'} | |
| tabIndex={activeTab === 'stack' ? 0 : -1} | |
| onClick={() => setActiveTab('stack')} | |
| onKeyDown={(event) => { | |
| if (event.key === 'Enter' || event.key === ' ') { | |
| event.preventDefault(); | |
| setActiveTab('stack'); | |
| } | |
| }} | |
| > | |
| <TabButton active={activeTab === 'stack'}> | |
| Stack | |
| </TabButton> | |
| </div> | |
| <div | |
| role="tab" | |
| aria-selected={activeTab === 'raw'} | |
| tabIndex={activeTab === 'raw' ? 0 : -1} | |
| onClick={() => setActiveTab('raw')} | |
| onKeyDown={(event) => { | |
| if (event.key === 'Enter' || event.key === ' ') { | |
| event.preventDefault(); | |
| setActiveTab('raw'); | |
| } | |
| }} | |
| > | |
| <TabButton active={activeTab === 'raw'}> | |
| Raw | |
| </TabButton> | |
| </div> |
| }} | ||
| > | ||
| <div | ||
| className="flex gap-1 border-b" | ||
| style={{ | ||
| borderColor: 'var(--ds-gray-300)', | ||
| backgroundColor: 'transparent', | ||
| }} | ||
| > | ||
| <TabButton | ||
| active={activeTab === 'stack'} | ||
| onClick={() => setActiveTab('stack')} | ||
| > | ||
| Stack |
There was a problem hiding this comment.
ErrorBodyWithTabs duplicates the same bordered container + header styling used in ConversationWithTabs. To reduce drift and make future styling changes easier, consider extracting a small shared wrapper component (or helper) for the tabbed border/header layout.
Slack-Thread: https://vercel.slack.com/archives/C09G3EQAL84/p1772083947691689?thread_ts=1772083947.691689&cid=C09G3EQAL84 Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
Remove unnecessary type cast and add missing toast import. Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
Error bodies containing structured errors with
stackfields were rendered as raw JSON tree objects, making stack traces hard to read. This affected the attribute panel, the sidebar events list, and the full events tab.What changed
ErrorStackBlockandisStructuredErrorWithStackinto a sharedui/error-stack-block.tsxcomponent for reuse across all error rendering surfacesattribute-panel.tsx): Renders errors with astackfield as readable<pre>text styled to matchCopyableDataBlock, falling back to the raw JSON viewer for errors without a stackevents-list.tsx): AddedEventDataBlockwrapper that detectsstep_failed/step_retryingevents and renders their nestederrorfield withErrorStackBlockevent-list-view.tsx): UpdatedPayloadBlockto accept aneventTypeprop and render structured errors fromstep_failed,step_retrying,run_failed, andworkflow_failedevents withErrorStackBlocktoastimport fromsonnerand removed incorrect type cast that widened the type-guard-narrowedvalue