Skip to content

ref(replays): Move replay details actions into the page-title menu - #123569

Open
Jesse-Box wants to merge 9 commits into
masterfrom
Jesse-Box/ref/breadcrumbList-replayDetails
Open

ref(replays): Move replay details actions into the page-title menu#123569
Jesse-Box wants to merge 9 commits into
masterfrom
Jesse-Box/ref/breadcrumbList-replayDetails

Conversation

@Jesse-Box

@Jesse-Box Jesse-Box commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Introduction

Consolidates a lot of the replay details page actions into a single actions menu on the page title, per DE-1419. Same shape as the trace view (#120794) and transaction summary (#123128) headers.

Screenshot Before

CleanShot 2026-09-03 at 15 41 50@2x

Screenshot After 1

CleanShot 2026-09-03 at 15 42 20@2x

Screenshot After 2

CleanShot 2026-09-03 at 15 42 49@2x

Whats specifically changed in replay details

  • Modified ReplayItemDropdown and ConfigureReplayCard become hooks returning menu entries; the configure doc links nest in a submenu, and the employee-only actions group into a labelled section.
  • Added copying the full replay ID — the page title only renders the shortened form, which the API and search syntax don't accept.
  • Removed copying a link to the current timestamp, because the functionality is also facilitated by the share modal.
  • Deleted ReplayDetailsHeaderActions, leaving the actions empty and the feedback button falls through to the top bar's own fallback. The live-refresh chip is restyled as a zero-size primary button.
  • The docs-clicked analytics event moves onto each configure link, since the menu-level handler it rode on now fires for the sibling actions too.

Whats changed in <BreadcrumbList/>

Bug fixed in BreadcrumbList, affecting other consumers: the page-title crumb keyed each trailingActions entry by its index after dropping the falsy ones, so an action appearing in an earlier slot remounted every later action and discarded its state — an open dropdown closed on its own as soon as the live-refresh chip rendered beside it. Now keyed on the slot an action was declared in. The trace and transaction-summary headers each pass a single action rather than an array, so neither changes behaviour today.

Fixes DE-1419

The page-title crumb invites consumers to inline conditionals in `trailingActions` and
drops the falsy entries for them, but it keyed each action by its index after that
filtering. An action appearing in an earlier slot therefore shifted every later action
onto a new key, remounting it and discarding its state.

In practice a dropdown left open would close on its own as soon as a conditional action
ahead of it rendered. Key on the slot an action was declared in instead, so filling a
slot no longer disturbs its siblings.

Refs DE-1419
The details header split its actions across the top bar: a "Configure Replay" dropdown
and an ellipsis dropdown sat in the `actions` slot, far from the replay they act on.
Fold both into a single actions menu rendered as a page-title trailing action, matching
the trace view and transaction summary headers.

`ReplayItemDropdown` and `ConfigureReplayCard` become hooks returning menu entries, with
the configure doc links nested in a submenu so the merged menu stays short. Their
subtitles move to the menu item's own `details` prop, dropping three styled components.
Docs-clicked tracking moves onto each link, since the menu-level handler it used to ride
on now fires for the sibling actions too.

Copying a link to the current timestamp is removed per the design. The `actions` slot is
now empty, so `ReplayDetailsHeaderActions` goes with it and the feedback button falls
through to the top bar's own fallback, which also tags feedback with its source. The
live-refresh chip is restyled as a zero-size primary button.

The replay poll mock in the details spec had no timestamps, so mapping it to a replay
record threw. It went unnoticed while every test was synchronous; the new ones await
user events and surface it.

Fixes DE-1419
Downloading the replay record, opening the replay debugger and downloading the first
video segment are all visible only to Sentry employees, but sat interleaved with the
actions everyone sees. Collect them into a "Sentry Employee Features" section at the end
of the menu, so it is obvious at a glance which actions others cannot see.

An item with `children` and no `submenu` renders as a labelled group, and the menu draws
the divider above it, so no manual separator is needed. The section drops out entirely
rather than rendering an empty heading when none of its actions apply.

The debug feature badges go with the move: the heading already says who these rows are
for, and the video-segment action never carried one.

Refs DE-1419
The page title shows a shortened replay ID, which is not what the API, the search
syntax or a support thread will accept. Nothing on the page offered the full one, so
reading it meant pulling it out of the URL.

Add a copy action at the top of the page-title menu that puts the full ID on the
clipboard, and disable it until the replay record has loaded.

Refs DE-1419
Sharing a replay is the far more common of the two, so it leads.

Refs DE-1419
@github-actions github-actions Bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Sep 3, 2026
@Jesse-Box
Jesse-Box marked this pull request as ready for review September 3, 2026 13:58
@Jesse-Box
Jesse-Box requested review from a team as code owners September 3, 2026 13:58
Comment thread static/app/views/explore/replays/detail/header/useReplayMenuItems.tsx Outdated
…thheld

The details header withholds the replay reader when a replay is only partially
readable, so the actions needing its frames disable themselves. `useReplayMenuItems`
derived `isMobile` from that same reader, so a mobile replay with processing errors
looked like a web one.

Two consequences: the video-segment download disappeared rather than rendering disabled,
and the Configure Replay submenu linked the JavaScript docs instead of the SDK's own —
the second affecting every viewer, not just employees. Read `isMobile` before the reader
is withheld and pass it in; a partially-readable replay is still a mobile one.

Refs DE-1419
@linear-code

linear-code Bot commented Sep 3, 2026

Copy link
Copy Markdown

DE-1419

@Jesse-Box

Copy link
Copy Markdown
Contributor Author

bugbot run

The rationale they carried is preserved in the commit messages for each change
and in the pull request description.
… slot

Without it the flatMap reads as a roundabout filter, and simplifying it back
restores the bug where an open dropdown closed on its own.
@priscilawebdev

Copy link
Copy Markdown
Member

do we plan to move the update button elsewhere later? or perhaps make it an icon only?

image

Comment on lines +283 to +316
it('keeps an open dropdown open when an action declared before it appears', async () => {
function Title({showUpdate}: {showUpdate: boolean}) {
return (
<BreadcrumbList.Title
item={{
type: 'page-title',
label: 'JAVASCRIPT-2X9',
trailingActions: [
showUpdate
? {type: 'button', element: <Button size="zero">Update</Button>}
: null,
{
type: 'menu',
triggerLabel: 'More actions',
items: [{key: 'delete', label: 'Delete'}],
},
],
}}
/>
);
}

const {rerender} = render(<Title showUpdate={false} />);

await userEvent.click(screen.getByRole('button', {name: 'More actions'}));
expect(
await screen.findByRole('menuitemradio', {name: 'Delete'})
).toBeInTheDocument();

rerender(<Title showUpdate />);

expect(screen.getByRole('button', {name: 'Update'})).toBeInTheDocument();
expect(screen.getByRole('menuitemradio', {name: 'Delete'})).toBeInTheDocument();
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
it('keeps an open dropdown open when an action declared before it appears', async () => {
function Title({showUpdate}: {showUpdate: boolean}) {
return (
<BreadcrumbList.Title
item={{
type: 'page-title',
label: 'JAVASCRIPT-2X9',
trailingActions: [
showUpdate
? {type: 'button', element: <Button size="zero">Update</Button>}
: null,
{
type: 'menu',
triggerLabel: 'More actions',
items: [{key: 'delete', label: 'Delete'}],
},
],
}}
/>
);
}
const {rerender} = render(<Title showUpdate={false} />);
await userEvent.click(screen.getByRole('button', {name: 'More actions'}));
expect(
await screen.findByRole('menuitemradio', {name: 'Delete'})
).toBeInTheDocument();
rerender(<Title showUpdate />);
expect(screen.getByRole('button', {name: 'Update'})).toBeInTheDocument();
expect(screen.getByRole('menuitemradio', {name: 'Delete'})).toBeInTheDocument();
});
it('keeps the menu open when the Update button appears to its right', async () => {
function TestTitle({showEarlierAction}: {showEarlierAction: boolean}) {
return (
<BreadcrumbList.Title
item={{
type: 'page-title',
label: 'JAVASCRIPT-2X9',
trailingActions: [
{
type: 'menu',
triggerLabel: 'More actions',
items: [{key: 'menu-item', label: 'Menu item'}],
},
showEarlierAction
? {
type: 'button',
element: <Button size="zero">Earlier action</Button>,
}
: null,
],
}}
/>
);
}
const {rerender} = render(<TestTitle showEarlierAction={false} />);
await userEvent.click(screen.getByRole('button', {name: 'More actions'}));
expect(
await screen.findByRole('menuitemradio', {name: 'Menu item'})
).toBeVisible();
rerender(<TestTitle showEarlierAction />);
expect(screen.getByRole('button', {name: 'Earlier action'})).toBeVisible();
// The visible menu item proves the dropdown retained its open state.
expect(screen.getByRole('menuitemradio', {name: 'Menu item'})).toBeVisible();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Held off on this one — I tried it and it stops catching the bug.

Your version declares the menu first, so the appearing button lands in the slot after it. Under the old keying (index assigned after the nulls are dropped) the menu keeps index 0 in both states, never remounts, and the test passes against the buggy implementation — I reverted the fix locally to check, and it went green. The remount only happens when the conditional action is declared before the menu, which is why it is arranged that way.

Took the rest though: renamed to TestTitle, clearer test name, and toBeVisible() over toBeInTheDocument().

Comment thread static/app/views/explore/replays/details.spec.tsx Outdated
Comment thread static/app/views/explore/replays/details.spec.tsx
Comment thread static/app/views/explore/replays/details.spec.tsx
Comment on lines +156 to +201
it('groups the employee-only actions into their own section', async () => {
ConfigStore.set(
'user',
UserFixture({
id: '1',
emails: [{id: '1', email: 'someone@sentry.io', is_verified: true}],
})
);

renderDetails();

await userEvent.click(screen.getByRole('button', {name: 'Replay Actions'}));

const section = await screen.findByRole('group', {
name: 'Sentry Employee Features',
});
expect(
screen.queryByRole('menuitemradio', {name: 'Sentry Employee Features'})
).not.toBeInTheDocument();

expect(
await screen.findByRole('menuitemradio', {name: 'Download Replay Record'})
).toBeInTheDocument();
expect(section).toContainElement(
screen.getByRole('menuitemradio', {name: 'Download Replay Record'})
);
expect(section).toContainElement(
screen.getByRole('menuitemradio', {name: /Sentry Replay Debugger/})
);

expect(section).not.toContainElement(
screen.getByRole('menuitemradio', {name: 'Download JSON'})
);

expect(
screen.getAllByRole('menuitemradio').map(el => el.textContent?.trim())
).toEqual([
'Copy replay ID to clipboard',
'Share',
'Download JSON',
'Delete',
'Configure Replay',
expect.stringContaining('Download Replay Record'),
expect.stringContaining('Sentry Replay Debugger'),
]);
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i think if you apply my suggestions above we can delete this one

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Kept it, but pulled it out into its own test. It pins two things within() cannot: that the employee section sorts last, and that Share precedes Download JSON — both explicit design decisions rather than incidental ordering.

Comment thread static/app/views/explore/replays/detail/header/useReplayMenuItems.spec.tsx Outdated
Comment thread static/app/components/replays/header/configureReplayMenuItem.spec.tsx Outdated
Comment thread static/app/components/replays/header/configureReplayMenuItem.spec.tsx Outdated
Comment thread static/app/components/replays/header/configureReplayMenuItem.spec.tsx Outdated
Comment thread static/app/components/replays/header/configureReplayMenuItem.spec.tsx Outdated

@priscilawebdev priscilawebdev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

left a few suggestions, but lgtm ...great work 🚀👏

Translate the download error messages, which were passed to addErrorMessage as bare
strings, and lift the missing-replay guards out of the try blocks that could not throw
for them.

Tests: spy on the clipboard rather than reassigning it onto navigator, scope the menu
assertions with within(), and split the ordering expectation out of the employee-section
test. The configuration submenu now asserts its documentation links rather than only
that the items are enabled.
@Jesse-Box

Copy link
Copy Markdown
Contributor Author

do we plan to move the update button elsewhere later? or perhaps make it an icon only?

I'd rather not touch this right now as I remember this being a heavily discussed UI piece when I was still working on the project. Having said that, I can easily imagine this being an IconButton.

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

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants