feat(scraps): Add Tooltip.Header, Grid, Row and Footer - #121983
Conversation
Story previewsPreview the stories changed in this PR on the Vercel deployment: Preview deployment: https://sentry-45sc79fr5.sentry.dev |
Tooltip content that is a row of labelled values rather than a sentence currently has to fight the overlay: it hardcodes `padding: md lg` and `text-align: center`, so a card wanting full-width sections has to cancel both with negative margins. That pattern is already hand-rolled twice, in logsTimeTooltip and breadcrumbsTimeline, and RelativeTime was about to be the third. Adds a `padding` prop (defaulting to the current `md lg`, so no existing tooltip changes) and four sections that own their own spacing. `Tooltip.Body` owns the grid and `Tooltip.Row` renders as `display: contents`, so a column stays aligned across rows even when one row's cell is wider than the same cell above it. Also forwards `padding` through InfoText, which is the path TimeSince takes to the tooltip.
72282b6 to
93523fd
Compare
The padding tests compared emotion class names between two renders, which proved the default matched an explicit `md lg` but never that `md lg` resolves to the values the hardcoded rule used to emit. The comment justifying that also claimed emotion rules are unreadable in tests, which is wrong: sentry-test/utils exports `getEmotionRules`, and infoText.spec.tsx already uses it. Also covers two things that had no test at all: that InfoText forwards `padding` to the tooltip, which is the whole reason it was added and what PR 3 depends on, and that a tooltip composed of sections still associates itself with its trigger for screen readers.
Addresses the review on #121983. - `Tooltip.Body` is now `Tooltip.Grid`, which says what it is rather than claiming to be the whole body of the tooltip. - `trailing` is now `trailingItems`, and `leadingItems` joins it on the header, footer and row, matching the pattern the rest of the system uses. - `Tooltip.Row` carries that pattern rather than being a bare `display: contents` passthrough, so a three column row is described rather than hand-placed. It stays a component because `display: contents` is what keeps a column aligned across rows. - Composing sections into `title` now drops the overlay's content padding on its own. The prop stays for what the detection cannot see: a component that renders sections internally is a single element from out here, which is the case `TimeSince` is about to be in.
Inferring the padding meant walking `title` with `React.Children`, which the
React docs discourage, and it never covered the case that motivated it. The
walk only sees what the tooltip was handed directly, so `title={<RelativeTime />}`
is one opaque element and that component has to pass `padding="0"` regardless.
It only helped when the sections were inlined at the call site, which is the
case that needs the help least.
So `padding` goes back to a plain prop defaulting to `md lg`, and composing
sections means passing `'0'` alongside them. The section renames stay.
## Summary Second of the three PRs from the #121854 review. Dominik's point was that scraps can't reach into sentry for implicit context, so this inverts the two things that decide how an absolute time is written. `@sentry/scraps/datetime` exports `DateTimeProvider`, which takes `{timezone, clockDisplay}`, plus `useTimezone()` and `useClockDisplay()`. `SentryDateTimeProvider` stays in sentry, reads `useUser`, and supplies the value. It's wired into `ScrapsProviders` next to the tracking and link providers. The clock lives here rather than in PR 3 because `DateTime` and `TimeSince` both read it from `useUser` directly, so moving only the timezone would have left them coupled to sentry and PR 3 unable to move `TimeSince`. ## Review feedback addressed - One `DateTimeContext` instead of separate timezone and clock contexts, so consumers wire up a single thing. - File is `@sentry/scraps/datetime`. - `useTimezone()` unchanged, so none of the existing call sites move. `useClockDisplay()` added alongside it. - `clock24Hours: boolean` became `clockDisplay: '12' | '24'`, which reads better than a negated boolean at the call site. ## Heads up on the browser fallback The fallback reads as configured timezone, then browser, but the browser half can't currently be reached and the docstring now says so. `UserSerializer` resolves the option to `SENTRY_DEFAULT_TIME_ZONE` before it leaves the API, so someone who never opened their settings is indistinguishable from someone who chose UTC. Auto-detect needs the API to preserve that state first, which is a backend PR. No org timezone, per Nate. ## Stack 1. #121983, the tooltip sections 2. **this PR**, the datetime provider 3. RelativeTime + TimeSince (#121854), rebased on both 1 and 2 are independent. 3 needs both. ## Test plan - `core/datetime.spec.tsx` covers the defaults, the provided value, and nesting. `scrapsProviders/datetime.spec.tsx` covers resolution from the user and the 24 hour mapping. - Three specs were setting the clock through `ConfigStore` and asserting on components that now read context. Fixed to provide the value. - `pnpm typecheck`, `lint:js`, `oxfmt`, `knip` clean. 5707 tests across components, scrapsProviders and profiling utils. Green, on a base current with master. `ScrapsProviders` wraps `appRoutes` above `App`, so moving the provider out of `AppProviders` widens coverage rather than narrowing it, and now also covers the `processInitQueue` path. ## Open question `feedbackTimestampsTooltip`, `eventCreatedTooltip`, `auditLogList` and the release cards still read `clock24Hours` from `useUser` directly. They don't go through `DateTime` or `TimeSince` so nothing breaks, and I kept this PR to what PR 3 needs. Happy to sweep them separately.
An optional prop that has to be remembered is the wrong shape for this: the overlay's padding and the sections' padding are two halves of one decision, so the API should make it rather than the caller. The overlay now pads itself always and stands that padding down when it contains a section, matched with `:has([data-tooltip-section])`. Doing it in CSS rather than by inspecting children is what makes it cover the case that motivated the prop. A component that renders the sections internally is one opaque element as far as the tooltip is concerned, so `React.Children` could never see into it, but the rendered DOM is the same either way and `:has` reads that. `:has` is already used across the app, including in compactSelect, slider and segmentedControl. `InfoText` no longer forwards `padding` either, since it only forwarded it to give `TimeSince` a way to reach this. The specs cover it in two halves, because jsdom resolves neither emotion's computed styles nor `:has`: one asserts the overlay carries the rule, the others assert the marker really is in the DOM for a section composed directly, for a section behind a component boundary, and not for a plain sentence.
Replaces the `:has()` version of this with the negative margin Nate asked for, so the mechanism is the one he proposed rather than one substituted for it. The overlay pads itself as before. Sections cancel that padding with a negative inline margin and re-apply it themselves, so a full width row or separator can reach the edges while its own content stays inset. Only the first and last section cancel the block padding: doing it on every one would collapse the space between two of them, since adjacent negative margins compound. `max-width` needs no adjustment. The overlay's border box stays capped, its content box is that minus the inline padding, and a section adds exactly that padding back, so a section spans the overlay's outer width and no wider. The shrink-to-fit case balances for the same reason: the section's negative margins cancel its own padding out of the intrinsic width. The prop is still gone, which was the point of all of this.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d13fe92. Configure here.
The block-start cancel keyed off `:first-child`, which never matched. `Overlay` renders `OverlayArrow` ahead of its content whenever `arrowProps` is passed, and `Tooltip` always passes it, so the first section is the second child and a structured card kept the overlay's top padding on top of its own. Selecting on a preceding section instead gets the same result without caring what else the overlay renders: every section cancels the top padding, and one that follows another puts it back. The old tests could not have caught this. They asserted the rules existed in the generated CSS, which they did, not that anything matched them — jsdom resolves neither emotion's computed styles nor selectors. The added test asserts the thing that is checkable and was the actual trap: a section is not the overlay's first child. Found by the Cursor bot.
| * </Fragment> | ||
| * } | ||
| * > | ||
| * {trigger} | ||
| * </Tooltip> | ||
| * ``` | ||
| * | ||
| * That holds wherever the sections are rendered from. A component that renders | ||
| * them internally is covered too, because its sections are still the overlay's | ||
| * own children in the DOM. | ||
| * |
There was a problem hiding this comment.
Bug: The Tooltip component's section styling breaks silently if child sections are wrapped in an element like a <div> instead of a Fragment, due to a direct-child CSS selector.
Severity: LOW
Suggested Fix
To make the API less fragile, either update the CSS to not rely on a direct child selector, or add a runtime development warning if the Tooltip's children are not the expected section components. At a minimum, explicitly document this limitation, warning users not to wrap sections in container elements.
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/core/tooltip/tooltip.tsx#L343-L353
Potential issue: The `Tooltip` component's styling for sections like `Header`, `Footer`,
and `Grid` relies on a direct child CSS selector (`> [data-tooltip-section]`) to apply
negative margins. If a developer wraps these sections in a container element like a
`<div>` instead of the documented `Fragment` pattern, the selector will not match. This
causes a silent failure where the negative margins are not applied, resulting in a
visually broken layout with unexpected padding around the sections. The component does
not provide any runtime warnings or errors in this scenario.
Did we get this right? 👍 / 👎 to inform future reviews.
natemoo-re
left a comment
There was a problem hiding this comment.
Looks great, thanks for addressing the feedback!
adds a `RelativeTime` card to `@sentry/scraps/relativeTime` — relative time in the header, absolute time in the viewer's timezone and UTC. Every `TimeSince` renders one, starting with the Last Seen and First Seen columns on the issue feed. **Before:** <img width="255" height="177" alt="image" src="https://github.com/user-attachments/assets/f98d705e-6961-4f20-8f34-f0538397a1ec" /> <img width="255" height="177" alt="image" src="https://github.com/user-attachments/assets/f1c08b85-d9b3-436a-bc47-5a404f5a73b2" /> **After:** Last seen <img width="255" height="177" alt="image" src="https://github.com/user-attachments/assets/039801fe-d39c-4010-8ea3-885d81215f16" /> First seen <img width="255" height="177" alt="image" src="https://github.com/user-attachments/assets/7952adf7-dad0-40f4-80ef-9e5ac0c8b5e1" /> If only UTC is set (user didnt update their timezone in settings yet): <img width="255" height="177" alt="image" src="https://github.com/user-attachments/assets/70b00b36-83ad-4948-8712-cdffa6f5b05d" /> ## Stack Top of a 3 PR stack. This one needs both of the others: 1. #121983, the tooltip sections 2. #122063, the datetime provider 3. **this PR**, RelativeTime + TimeSince ## Review feedback addressed - **Scraps no longer reaches into sentry for the timezone.** `@sentry/scraps/datetime` owns the provider and sentry supplies the value, in #122063. Same inversion as the tracking and link providers. - **`TimeSince` always renders `RelativeTime`.** There's no way to get one without the other now. `tooltipBody` remains for the callers that replace the overlay wholesale. - **Two rows, never three.** Viewer's timezone on top, UTC underneath. ## Open question **Does `TimeSince` move into scraps in this PR, or the next one?** Nothing blocks it: the only sentry import left in the file is `t` from `sentry/locale`, which 35 components already in scraps import, `RelativeTime` among them. So it's a file move plus rewriting 87 imports. That's ~90 mechanically changed files, which would bury the part of this PR worth actually reading, so I'd rather be told which you'd prefer to review. ## Known gap The provider falls back to the browser's timezone when the viewer hasn't configured one, but that branch can't be reached today. `UserSerializer` resolves the option to `SENTRY_DEFAULT_TIME_ZONE` before it leaves the API, so someone who never opened their settings is indistinguishable from someone who chose UTC. Auto-detect needs the API to preserve that state first, which is a backend PR. No org timezone: neither the `Organization` type nor its serializer carries one, and per Nate we'd rather not add a property to the org for this. story: `relativeTime.mdx` <!-- [live story](https://sentry-git-cv-scraps-relative-time.sentry.dev/stories/core/relativeTime) --> <!-- Closes DE-1513 -->

Summary
Splitting #121854 into three PRs based on the review feedback. This is the first one, and it only adds the tooltip primitives so the RelativeTime card has something to build on.
TooltipContenthardcodedpadding: md lgandtext-align: center. That's fine for a sentence but wrong for a card, so anything card shaped had to cancel both by hand. We already do that in three places:logsTimeTooltip,breadcrumbsTimeline, andgroup.tsx. RelativeTime was about to be the fourth.Tooltip.Header/Footertake a label plus optionalleadingItemsandtrailingItems. Sentence case, no bottom border.Tooltip.Gridowns the grid the rows line up against.Tooltip.RowtakesleadingItemsandtrailingItemseither side of its main cell, and renders asdisplay: contentsso all three become items of the grid itself. That's what keeps columns aligned across rows when one cell is wider than the one above it.Primitives only here, nothing migrated yet.
Review feedback addressed
trailingistrailingItems, andleadingItemsexists onHeader,RowandFooter.Tooltip.BodyisTooltip.Grid, which is what it actually is.paddingprop is gone. Optional guidance gets forgotten, so the component handles the layout instead of asking the caller to.max-widthneeds no adjustment. The overlay's border box stays capped at 225, its content box is that minus the inline padding, and a section adds exactly that padding back, so it spans the overlay's outer width and no wider. Shrink-to-fit balances for the same reason. Worth confirming on the story preview, since jsdom does no layout.display: contentsstays onTooltip.Row. Checked whether subgrid is used elsewhere first: it is, in 31 files, andcore/tableuses this exact recipe. Butdisplay: contentswas fine either way, so it stays.InfoTextno longer forwardspadding, since it only did so to giveTimeSincea way to reach this.Stack
Bottom of a 3 PR stack:
Worth writing down
leadingItems/trailingItemsonTooltip.Roware optional, so a row emitting two cells where its neighbours emit three shifts every following row one track over, and it cascades.HeaderandFooterguard against that withdefined(trailingItems);Rowcan't, because underdisplay: contentsevery cell of every row is one placement stream. Subgrid would contain it to the row. Nothing shipping hits this today, sinceRelativeTimealways emits three cells.tooltip.mdx.