Conversation
- handles long filenames by wrapping to two lines - handles mobile screen sizes a little better - has two contexts instead of prop drilling - does not use global styles
Move frame context and variables rendering further onto layout/text primitives and wire frame context regions to chevron controls for better semantics. Avoid work for collapsed frames by gating source context/highlighting inputs behind expansion state. Add keyboard-focused tests for chevron Enter/Space behavior and aria wiring. Co-Authored-By: Claude <noreply@anthropic.com>
Extract frame context and variables rendering into reusable stacktrace units and tighten header/layout interactions for the new component API. Add a coverage host that uses React Query query options and query cache, fetching coverage only for expanded frames while keeping the base StackTrace component data-source agnostic.
Fix Core StackTrace spec failures by adding default stacktrace-link mocks, making badge assertions resilient, and using explicit space key events for chevron keyboard behavior. Also remove unused stacktrace exports and simplify coverage query eligibility checks without changing behavior.
Lift toolbar view state (view, isNewestFirst, isMinified) out of per-exception StackTraceProviders into a single StackTraceSharedViewContext. ChainedStackTrace now renders one toolbar above all exceptions so switching App/Full/Raw or toggling order affects all frames simultaneously. - Add StackTraceSharedViewContext + StackTraceSharedViewProvider - StackTraceProvider.Root falls back to local state when no shared context is present (existing single-trace usage unchanged) - Toolbar sub-components read shared context first, then per-provider context, enabling standalone use in StackTraceSharedViewProvider Co-Authored-By: Claude <noreply@anthropic.com>
mix-blend-mode: screen washed out coverage colors to near-white when blending against background.secondary (a light gray). The old component used var(--prism-highlight-background) which is dark enough for screen blending to produce visible results. Since FrameSourceLineNumber is a grid cell, its background already covers the row highlight naturally — no blending is needed. Remove mix-blend-mode and apply coverage colors directly, using the 200-level shades for active lines to distinguish from the 100-level shades on inactive lines. Co-Authored-By: Claude <noreply@anthropic.com>
Pull all frame actions (chevron, source link, source maps debugger, hidden frames toggle) out of the monolithic FrameHeader into individual files under frame/actions/, mirroring the toolbar/ pattern. Expose them via StackTraceProvider.Frame.Actions.* so consumers can compose exactly the actions they need. FrameHeader gains an `actions` prop — when omitted it renders the same default set as before, so all existing usage is unchanged. Also extracts toolbar items (DisplayOptions, CopyButton, DownloadButton) into toolbar/, adds ExceptionHeader as a standalone composable component, and isolates hover state into a separate StackTraceFrameHoverContext so only action components re-render on hover rather than the full frame tree. Co-Authored-By: Claude <noreply@anthropic.com>
…ckTrace Replace the standalone ChainedStackTrace component with IssueStackTrace, a single cohesive component that handles both single and chained exceptions. IssueStackTrace owns the InterimSection chrome (title, actions in header) for both cases and provides a CopyButton that concatenates all stacktraces in the chained case. Move SharedViewRoot out of stackTraceProvider into issueStackTrace so the provider stays a pure generic component with no knowledge of the issues layer. Remove StackTraceSharedViewProvider export entirely. Co-Authored-By: Claude <noreply@anthropic.com>
Remove lockAddress and threadId from the generic StackTraceProvider context. These were ANR-specific concerns leaking into a generic component. Replace with a frameBadge render prop that lets callers inject per-frame badges without the provider knowing about ANR. IssueStackTrace now owns the ANR detection logic and passes a frameBadge callback that computes the Suspect Frame badge for qualifying frames. Also fixes the coverage line number aria-label for accessibility. Co-Authored-By: Claude <noreply@anthropic.com>
…x minified story - Document all StackTraceProviderProps with JSDoc comments - Rename Display Options toggle from "Unsymbolicated" to "Minified" to match the internal value name and be more discoverable - Update the minified story to use StackTraceProvider directly with defaultIsMinified so the feature is visible on load Co-Authored-By: Claude <noreply@anthropic.com>
Introduce a dedicated view-state provider and require stack-trace consumers to read from a single context source to remove cross-context fallbacks. Replace hover context with explicit props, tighten shared type contracts, and document context field guarantees so stack-trace APIs are easier to reason about. Co-Authored-By: Claude <noreply@anthropic.com> Made-with: Cursor
Move frame expansion state out of shared stacktrace context so toggling one frame updates only per-row consumers. Also hoist project lookup to provider scope and use set-based index membership in row building to cut repeated per-frame work. Co-Authored-By: Claude <noreply@anthropic.com> Made-with: Cursor
Restore StackTraceProvider.Frame to its public row-based API so composed frame stories compile without internal expansion props. Also remove a dead nullable context check in DownloadButton and align minifiedStacktrace docs with the new view-state provider model. Co-Authored-By: Claude <noreply@anthropic.com> Made-with: Cursor
Split frame rendering and default actions into dedicated stack trace modules and update the frame header to rely on CSS truncation with selectable inline text. This keeps path suffixes visible, improves copy/paste behavior, and stabilizes single-line and wrapped alignment. Co-Authored-By: Claude <noreply@anthropic.com> Made-with: Cursor
Use shared Text primitives and simplify inline metadata wrappers so the frame title row keeps consistent spacing and baseline alignment. Co-Authored-By: Claude <noreply@anthropic.com> Made-with: Cursor
Render chevrons as non-interactive indicators and reserve slot width only when visible rows include expandable frames. Keep action columns aligned across mixed rows and add a Storybook example to verify the behavior quickly. Co-Authored-By: Claude <noreply@anthropic.com> Made-with: Cursor
Components is static data from SentryAppComponentsStore that never changes within a stack trace lifecycle. Having it in context was just indirect prop drilling. IssueSourceLinkAction now reads directly from the store, and components is removed from the context, provider props, and provider value. Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com> Made-with: Cursor
Use React Activity to preserve frame state across expand/collapse and app/full view toggles instead of unmounting. Frames are lazily rendered on first appearance and kept alive via Activity when hidden. - Wrap FrameContent in Activity so expand/collapse preserves DOM and query cache state instead of remounting - Compute allRows (all frames) alongside filtered rows, render from allRows with Activity mode based on current visibility - Remove useEffect that reset hiddenFrameToggleMap on view change, toggle state now naturally persists across view switches - Remove early return null from IssueStackTraceFrameContext since FrameContent handles its own visibility - Update tests to use toBeVisible assertions since Activity keeps elements in the DOM with display:none Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| allRows, | ||
| exceptionIndex, | ||
| event, | ||
| frameSourceMapDebuggerData, |
There was a problem hiding this comment.
Bug: The getDefaultPlatform function incorrectly prioritizes event.platform over the more specific frame.platform, which is a regression from the previous logic and inconsistent with the backend.
Severity: MEDIUM
Suggested Fix
In stackTraceProvider.tsx, modify the getDefaultPlatform function to prioritize the frame-level platform before falling back to the event-level platform. Change the return statement to return framePlatform ?? event.platform ?? 'other'; to align with the old implementation and backend logic.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: static/app/components/stackTrace/stackTraceProvider.tsx#L139-L142
Potential issue: The `getDefaultPlatform` function in the new stack trace provider
incorrectly determines the platform for rendering frames. It prioritizes the event-level
platform (`event.platform`) over the more specific frame-level platform
(`frame.platform`). This is a regression from the previous implementation and is
inconsistent with the backend's grouping logic, which both prioritize the frame's
platform. This can lead to incorrect platform-specific rendering for stack trace frames,
such as applying the wrong line number formatting or other platform-specific UI features
when a frame's platform differs from the overall event's platform.
Did we get this right? 👍 / 👎 to inform future reviews.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Chained exception copy ignores minified toggle state
- The chained-exception copy path now uses the same minified-versus-symbolicated stacktrace selection as the rendered raw view and is covered by a regression test.
- ✅ Fixed: Hidden frame toggle state not reset on frame change
- Hidden-frame expansion state is now scoped to the active stacktrace/view so switching variants resets stale groups immediately, with regression coverage added.
Or push these changes by commenting:
@cursor push 359f8d2144
Preview (359f8d2144)
diff --git a/static/app/components/stackTrace/issueStackTrace/index.spec.tsx b/static/app/components/stackTrace/issueStackTrace/index.spec.tsx
--- a/static/app/components/stackTrace/issueStackTrace/index.spec.tsx
+++ b/static/app/components/stackTrace/issueStackTrace/index.spec.tsx
@@ -407,6 +407,68 @@
expect(firstIdx).toBeLessThan(secondIdx);
});
+ it('copies unsymbolicated raw text for chained exceptions when minified view is active', async () => {
+ Object.assign(navigator, {
+ clipboard: {
+ writeText: jest.fn().mockResolvedValue(''),
+ },
+ });
+
+ const {event, stacktrace} = makeStackTraceData();
+ const symbolicatedStacktrace: StacktraceWithFrames = {
+ ...stacktrace,
+ frames: stacktrace.frames.map((frame, index) => ({
+ ...frame,
+ filename: `symbolicated/${index}.js`,
+ })),
+ };
+ const rawStacktrace: StacktraceWithFrames = {
+ ...symbolicatedStacktrace,
+ frames: symbolicatedStacktrace.frames.map((frame, index) => ({
+ ...frame,
+ filename: `minified/${index}.js`,
+ })),
+ };
+
+ render(
+ <IssueStackTrace
+ event={event}
+ values={[
+ {
+ type: 'RootError',
+ value: 'root cause',
+ module: 'app.main',
+ mechanism: {handled: false, type: 'generic'},
+ stacktrace: symbolicatedStacktrace,
+ rawStacktrace,
+ threadId: null,
+ },
+ {
+ type: 'NestedError',
+ value: 'nested cause',
+ module: 'app.nested',
+ mechanism: {handled: false, type: 'generic'},
+ stacktrace: symbolicatedStacktrace,
+ rawStacktrace,
+ threadId: null,
+ },
+ ]}
+ />
+ );
+
+ await userEvent.click(screen.getByRole('button', {name: 'Display options'}));
+ await userEvent.click(await screen.findByRole('option', {name: 'Unsymbolicated'}));
+
+ await userEvent.click(screen.getByRole('button', {name: 'Copy as'}));
+ await userEvent.click(await screen.findByText('Text'));
+
+ await waitFor(() => expect(navigator.clipboard.writeText).toHaveBeenCalled());
+
+ const copiedText = (navigator.clipboard.writeText as jest.Mock).mock.calls[0][0];
+ expect(copiedText).toContain('minified/0.js');
+ expect(copiedText).not.toContain('symbolicated/0.js');
+ });
+
describe('standalone stacktrace prop', () => {
it('renders frame rows for a standalone stacktrace', async () => {
const {event, stacktrace} = makeStackTraceData();
diff --git a/static/app/components/stackTrace/issueStackTrace/index.tsx b/static/app/components/stackTrace/issueStackTrace/index.tsx
--- a/static/app/components/stackTrace/issueStackTrace/index.tsx
+++ b/static/app/components/stackTrace/issueStackTrace/index.tsx
@@ -77,6 +77,13 @@
stacktrace: StacktraceType;
}
+function getDisplayedRawStacktrace(
+ exc: IndexedExceptionValue,
+ isMinified: boolean
+): StacktraceType {
+ return isMinified ? (exc.rawStacktrace ?? exc.stacktrace) : exc.stacktrace;
+}
+
/** Resolves symbolicated vs raw (minified) exception fields. */
function resolveExceptionFields(exc: IndexedExceptionValue, isMinified: boolean) {
return {
@@ -251,8 +258,10 @@
exceptions
.map(exc =>
rawStacktraceContent({
- data: exc.stacktrace,
+ data: getDisplayedRawStacktrace(exc, isMinified),
platform: event.platform,
+ exception: exc,
+ isMinified,
})
)
.join('\n\n')
@@ -268,9 +277,7 @@
{exceptions
.map(exc =>
rawStacktraceContent({
- data: isMinified
- ? (exc.rawStacktrace ?? exc.stacktrace)
- : exc.stacktrace,
+ data: getDisplayedRawStacktrace(exc, isMinified),
platform: event.platform,
exception: exc,
isMinified,
diff --git a/static/app/components/stackTrace/stackTrace.spec.tsx b/static/app/components/stackTrace/stackTrace.spec.tsx
--- a/static/app/components/stackTrace/stackTrace.spec.tsx
+++ b/static/app/components/stackTrace/stackTrace.spec.tsx
@@ -245,6 +245,47 @@
expect(screen.getByRole('button', {name: 'Hide 1 frame'})).toBeInTheDocument();
});
+ it('resets hidden frame toggles when switching stacktrace variants', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ const symbolicatedStacktrace: StacktraceWithFrames = {
+ ...stacktrace,
+ frames: [
+ {...stacktrace.frames[0]!, filename: 'symbolicated/hidden.js', inApp: false},
+ {...stacktrace.frames[1]!, filename: 'symbolicated/visible.js', inApp: false},
+ {...stacktrace.frames[2]!, filename: 'symbolicated/app.js', inApp: true},
+ ],
+ };
+ const minifiedStacktrace: StacktraceWithFrames = {
+ ...symbolicatedStacktrace,
+ frames: [
+ {...symbolicatedStacktrace.frames[0]!, filename: 'minified/hidden.js'},
+ {...symbolicatedStacktrace.frames[1]!, filename: 'minified/visible.js'},
+ {...symbolicatedStacktrace.frames[2]!, filename: 'minified/app.js'},
+ ],
+ };
+
+ render(
+ <TestStackTraceProvider
+ event={event}
+ stacktrace={symbolicatedStacktrace}
+ minifiedStacktrace={minifiedStacktrace}
+ >
+ <DisplayOptions />
+ <StackTraceFrames frameContextComponent={FrameContent} />
+ </TestStackTraceProvider>
+ );
+
+ await userEvent.click(screen.getByRole('button', {name: 'Show 1 more frame'}));
+ expect(screen.getByText('symbolicated/hidden.js')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole('button', {name: 'Display options'}));
+ await userEvent.click(await screen.findByRole('option', {name: 'Unsymbolicated'}));
+
+ expect(screen.getByText('minified/hidden.js')).not.toBeVisible();
+ expect(screen.queryByRole('button', {name: 'Hide 1 frame'})).not.toBeInTheDocument();
+ expect(screen.getByRole('button', {name: 'Show 1 more frame'})).toBeInTheDocument();
+ });
+
it('renders frame badges for in-app frames only', async () => {
renderStackTrace();
diff --git a/static/app/components/stackTrace/stackTraceProvider.tsx b/static/app/components/stackTrace/stackTraceProvider.tsx
--- a/static/app/components/stackTrace/stackTraceProvider.tsx
+++ b/static/app/components/stackTrace/stackTraceProvider.tsx
@@ -45,12 +45,23 @@
);
const lastFrameIndex = useMemo(() => getLastFrameIndex(frames), [frames]);
- const [hiddenFrameToggleMap, setHiddenFrameToggleMap] = useState(() =>
- createInitialHiddenFrameToggleMap(frames, view === 'full')
+ const shouldIncludeSystemFrames = view === 'full';
+ const initialHiddenFrameToggleMap = useMemo(
+ () => createInitialHiddenFrameToggleMap(frames, shouldIncludeSystemFrames),
+ [frames, shouldIncludeSystemFrames]
);
+ const [hiddenFrameToggleState, setHiddenFrameToggleState] = useState(() => ({
+ stacktrace: activeStacktrace,
+ view,
+ map: initialHiddenFrameToggleMap,
+ }));
+ const hiddenFrameToggleMap =
+ hiddenFrameToggleState.stacktrace === activeStacktrace &&
+ hiddenFrameToggleState.view === view
+ ? hiddenFrameToggleState.map
+ : initialHiddenFrameToggleMap;
const platform = platformProp ?? getDefaultPlatform(activeStacktrace, event);
- const shouldIncludeSystemFrames = view === 'full';
const frameCountMap = useMemo(
() => getFrameCountMap(frames, shouldIncludeSystemFrames),
@@ -129,10 +140,21 @@
hiddenFrameToggleMap,
lastFrameIndex,
toggleHiddenFrames: (frameIndex: number) => {
- setHiddenFrameToggleMap(prevState => ({
- ...prevState,
- [frameIndex]: !prevState[frameIndex],
- }));
+ setHiddenFrameToggleState(prevState => {
+ const currentMap =
+ prevState.stacktrace === activeStacktrace && prevState.view === view
+ ? prevState.map
+ : initialHiddenFrameToggleMap;
+
+ return {
+ stacktrace: activeStacktrace,
+ view,
+ map: {
+ ...currentMap,
+ [frameIndex]: !currentMap[frameIndex],
+ },
+ };
+ });
},
}),
[
@@ -144,12 +166,14 @@
hasAnyExpandableFrames,
hideSourceMapDebugger,
hiddenFrameToggleMap,
+ initialHiddenFrameToggleMap,
lastFrameIndex,
meta,
platform,
project,
rows,
activeStacktrace,
+ view,
]
);This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
…ptions The chained-exception copy button always copied the symbolicated stacktrace, ignoring the isMinified toggle. Now both single and chained exception copy use the same minified-aware data source. Also removes the CopyButton wrapper in favor of inline CopyAsDropdown, and adds a test covering the unsymbolicated copy path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract shared sectionActions and narrow StackTraceProvider to wrap only StackTraceFrames in the single-exception branch, matching the multi-exception structure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| const [hiddenFrameToggleMap, setHiddenFrameToggleMap] = useState(() => | ||
| createInitialHiddenFrameToggleMap(frames, view === 'full') | ||
| ); |
There was a problem hiding this comment.
Bug: The hiddenFrameToggleMap state is not reset when frames change, causing stale indices when toggling between minified and symbolicated views.
Severity: MEDIUM
Suggested Fix
Reset the hiddenFrameToggleMap state whenever the frames prop changes. This can be achieved by using a useEffect hook that listens for changes to frames and calls setHiddenFrameToggleMap to re-initialize the state based on the new frames.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: static/app/components/stackTrace/stackTraceProvider.tsx#L48-L50
Potential issue: In the `StackTraceProvider` component, the `hiddenFrameToggleMap` state
is initialized only once when the component mounts. When the user toggles between
minified and symbolicated stack traces, the `frames` array changes, but
`hiddenFrameToggleMap` is not updated. This causes it to retain stale frame indices from
the previous stack trace. If the minified and symbolicated stack traces have different
frame counts or structures, any subsequent attempt to toggle hidden frames will use
incorrect indices, leading to the wrong frames being shown or hidden, or the toggle
having no effect.
- Import `isRepeatedFrame` and `getLastFrameIndex` from existing `events/interfaces/utils` instead of duplicating them in getRows.tsx - Extract `toggleHiddenFrames` to `useCallback` to avoid recreating the closure on every context useMemo recomputation - Fix `event: any` type to `Event` in FrameLocation props - Deduplicate `leadsToApp`/`hasLeadHint` condition in frameHeader - Reuse `formatFrameLocation` for inline suffix computation - Move shared `RawStackTraceText` styled component to rawStackTrace.tsx - Remove unnecessary wrapper `<div>` elements in ExceptionHeader and StackTraceFrames - Inline visibility conditions in DefaultFrameActions and IssueFrameActions so rendering logic is visible at composition level - Replace `.filter().flat()` with `.flatMap()` in getRows
The banner was rendered when `idx === 0`, but the exceptions array is reversed when newest-first. Use `firstVisibleExceptionIndex` instead, which accounts for both sort order and hidden exception groups.
Remove margin-bottom from FramesPanel and add a `borderless` prop that strips border and border-radius for embedded contexts like hover previews.
When a frame's absPath is an HTTP URL, show it as a clickable link in the filename tooltip. Extracted tooltip rendering into a FrameLocationTooltip subcomponent that owns the Tooltip, content, and disabled logic.


New stack trace component (non-native):
Components are now broken into IssueStackTrace and StackTrace. All fetch requests happen in IssueStackTrace and StackTrace components are somewhat designed to be compostable. It got tricky in there.
stories - https://sentry-9kfjix4ca.sentry.dev/stories/product/components/stacktrace/stacktrace/