From b3f6159281ec7156203a3f0fa403617850fe3f34 Mon Sep 17 00:00:00 2001 From: Catalin Irimie Date: Tue, 25 Aug 2026 02:19:42 +0300 Subject: [PATCH 1/3] fix(error-tracking): stop exception card header label overlapping the tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header's label cell carried `min-w-0` with nothing clipping it, so once its grid track collapsed the "Exception" label painted straight over the tab bar instead of disappearing. The symmetric `1fr auto 1fr` template brought that on early: it reserves two equal gutters to centre the tab bar even though only one gutter has content. Clip the label cell, and degrade the header on the card's own width — a container query, since the card lives in a resizable split pane where viewport breakpoints say nothing useful. Below @xl the label drops to sr-only and the tab bar takes the slack; at @xl and up the symmetric gutters and centred tab bar come back. The action cell sits in an `auto` track so it never shrinks, and the tab bar scrolls (justify-start, so a centred overflow can't put the first tab out of reach) as a last valve. Adds ExceptionCardHeaderWidths and ExceptionCardHeaderWidthsWithAction stories pinning each branch, the second with the right-hand action occupied. --- .../ExceptionCard/ExceptionCard.stories.tsx | 104 +++++++++++++++++- .../ExceptionCard/ExceptionCard.tsx | 26 ++++- 2 files changed, 122 insertions(+), 8 deletions(-) diff --git a/products/error_tracking/frontend/components/ExceptionCard/ExceptionCard.stories.tsx b/products/error_tracking/frontend/components/ExceptionCard/ExceptionCard.stories.tsx index cd4baa1f520f..f3602fb5350e 100644 --- a/products/error_tracking/frontend/components/ExceptionCard/ExceptionCard.stories.tsx +++ b/products/error_tracking/frontend/components/ExceptionCard/ExceptionCard.stories.tsx @@ -5,8 +5,10 @@ import { useEffect } from 'react' import { LemonCard } from '@posthog/lemon-ui' import { ErrorEventType } from 'lib/components/Errors/types' +import { FEATURE_FLAGS } from 'lib/constants' import { mswDecorator } from '~/mocks/browser' +import type { Mocks } from '~/mocks/utils' import { NodeKind } from '~/queries/schema/schema-general' import { TEST_EVENTS } from '../../__mocks__/events' @@ -549,8 +551,8 @@ function buildSessionTimelineEvent( } } -function OpenTimelineTab({ children }: { children: JSX.Element }): JSX.Element { - const { setCurrentTab } = useActions(exceptionCardLogic({ issueId: 'issue-id', loading: false })) +function OpenTimelineTab({ children, issueId = 'issue-id' }: { children: JSX.Element; issueId?: string }): JSX.Element { + const { setCurrentTab } = useActions(exceptionCardLogic({ issueId, loading: false })) useEffect(() => { setCurrentTab('timeline') @@ -588,3 +590,101 @@ export function ExceptionCardAllEvents(): JSX.Element { ) } + +//////////////////// Header layout + +/* + * The header packs a label, the tab bar and an action into one 40px row. It used to overlap: the + * label's grid track was allowed to collapse (min-w-0) while nothing clipped the text, so "Exception" + * painted straight over "Stack Trace". These widths pin each branch of the container query. + */ +const HEADER_WIDTHS: { width: number; caption: string }[] = [ + { width: 300, caption: '300px — tightest pane: icon only, tab bar scrolls' }, + { width: 420, caption: '420px — icon only, tab bar takes the slack' }, + { width: 560, caption: '560px — still under @xl: icon only' }, + { width: 576, caption: '576px — @xl boundary: label is back, tab bar re-centres' }, + { width: 900, caption: '900px — roomy: symmetric gutters, tab bar dead centre' }, +] + +function HeaderWidthMatrix({ + widths = HEADER_WIDTHS, + children, +}: { + widths?: typeof HEADER_WIDTHS + children: (width: number) => JSX.Element +}): JSX.Element { + return ( +
+ {widths.map(({ width, caption }) => ( +
+
{caption}
+ {/* outline rather than border: a border eats 2px of the card's width and would shift + the container-query boundary these captions are naming. */} +
+ {children(width)} +
+
+ ))} +
+ ) +} + +export function ExceptionCardHeaderWidths(): JSX.Element { + const event = asErrorEventType(TEST_EVENTS['javascript_resolved']) + + return ( + + {(width) => ( + + )} + + ) +} + +/* + * Worst case: the right-hand cell is occupied too, so the tab bar has the least room it ever gets in + * production. The action has to survive at every width — the label is the only thing allowed to go. + */ +export function ExceptionCardHeaderWidthsWithAction(): JSX.Element { + const event = asErrorEventType(TEST_EVENTS['javascript_resolved']) + + return ( + width <= 576)}> + {(width) => ( + + + + )} + + ) +} +ExceptionCardHeaderWidthsWithAction.parameters = headerActionParameters() + +// The action is a ViewLogsButton: feature-flagged, only rendered on the timeline/recording tabs, and +// gated on the team's logs config. All three have to be satisfied or the right-hand cell renders empty +// and the story stops testing the crowded case it exists for. +function headerActionParameters(): Record { + const timeline = sessionTimelineParameters(asErrorEventType(TEST_EVENTS['javascript_resolved'])) + const timelineMocks = (timeline.msw as { mocks: Mocks }).mocks + + return { + featureFlags: [FEATURE_FLAGS.LOGS_IN_ERROR_TRACKING], + msw: { + mocks: { + ...timelineMocks, + get: { + 'api/projects/:team_id/logs_config/': { + logs_distinct_id_attribute_key: 'posthogDistinctId', + logs_distinct_id_attribute_keys: ['posthogDistinctId'], + logs_session_id_attribute_keys: ['sessionId'], + }, + }, + }, + }, + } +} diff --git a/products/error_tracking/frontend/components/ExceptionCard/ExceptionCard.tsx b/products/error_tracking/frontend/components/ExceptionCard/ExceptionCard.tsx index 38158cbcd274..4b74474e32e7 100644 --- a/products/error_tracking/frontend/components/ExceptionCard/ExceptionCard.tsx +++ b/products/error_tracking/frontend/components/ExceptionCard/ExceptionCard.tsx @@ -93,14 +93,28 @@ function ExceptionCardContent({ setCurrentTab(value) } }} - className="flex min-h-0 flex-1 flex-col gap-0" + className="@container/exception-card flex min-h-0 flex-1 flex-col gap-0" > -
-
- - Exception + {/* Container query, not viewport: the card lives in a resizable split pane. Wide — symmetric + 1fr gutters centre the tab bar. Narrow — the gutters shrink to their content and the tab + bar takes the slack; reserving two gutters for one label is what squeezed it. */} +
+ {/* overflow-hidden is load-bearing: min-w-0 lets this track collapse, and with nothing + clipping it the label painted straight over the tab bar. */} +
+ + {/* sr-only rather than hidden: no room to draw it, but it stays in the a11y tree. */} + Exception
- + {/* Last valve: under ~390px the tab bar alone doesn't fit, so scroll it rather than let + it spill into the actions. justify-start overrides the list's own justify-center — + a centred flex row that overflows spills off *both* edges, putting the first tab + out of reach of the scroller. */} + Date: Tue, 25 Aug 2026 03:17:32 +0300 Subject: [PATCH 2/3] fix(error-tracking): scope the exception card header container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes from review on the header layout change. Inline-size containment makes an element's width independent of its contents, so it contributes nothing to a shrink-to-fit ancestor. With the container on the Tabs root that covered the whole card, which collapsed it to zero wherever its width came from its content — visual review caught this on `exception-card-with-footer`, rendering at 32px instead of 570px. The container now wraps the header alone, so the card is still sized by its body. That story also gains a definite width, like its siblings: `w-full` resolves against a shrink-to-fit storybook wrapper, so the card was sizing to its own content rather than to the story. Base UI scrolls the active tab into view on mount and on keyboard navigation, but not when the value changes from elsewhere. The timeline switches to the recording tab on a timestamp click, which in a narrow pane left the active tab outside the scroller with no scrollbar to reveal it. Nudging scrollLeft rather than calling scrollIntoView keeps ancestors where they are. Comments no longer narrate what the layout used to do, per the AGENTS.md rule against change history in source; that belongs in the commit and the PR. --- .../ExceptionCard/ExceptionCard.stories.tsx | 13 +- .../ExceptionCard/ExceptionCard.tsx | 142 +++++++++++------- 2 files changed, 91 insertions(+), 64 deletions(-) diff --git a/products/error_tracking/frontend/components/ExceptionCard/ExceptionCard.stories.tsx b/products/error_tracking/frontend/components/ExceptionCard/ExceptionCard.stories.tsx index f3602fb5350e..ebbe57be93ac 100644 --- a/products/error_tracking/frontend/components/ExceptionCard/ExceptionCard.stories.tsx +++ b/products/error_tracking/frontend/components/ExceptionCard/ExceptionCard.stories.tsx @@ -68,8 +68,10 @@ ExceptionCardBase.parameters = sessionTimelineParameters(asErrorEventType(TEST_E export function ExceptionCardWithFooter(): JSX.Element { const event = asErrorEventType(TEST_EVENTS['javascript_resolved']) + // Definite width, like the other card stories: `w-full` resolves against a shrink-to-fit storybook + // wrapper, so the card would size to its own content rather than to the story. return ( -
+
(
{caption}
- {/* outline rather than border: a border eats 2px of the card's width and would shift - the container-query boundary these captions are naming. */} + {/* outline rather than border: a border eats 2px of the card's width and shifts the + container-query boundary these captions name. */}
{children(width)}
diff --git a/products/error_tracking/frontend/components/ExceptionCard/ExceptionCard.tsx b/products/error_tracking/frontend/components/ExceptionCard/ExceptionCard.tsx index 4b74474e32e7..a235721fb549 100644 --- a/products/error_tracking/frontend/components/ExceptionCard/ExceptionCard.tsx +++ b/products/error_tracking/frontend/components/ExceptionCard/ExceptionCard.tsx @@ -1,5 +1,5 @@ import { BindLogic, useActions, useValues } from 'kea' -import { useMemo, type CSSProperties } from 'react' +import { useEffect, useMemo, useRef, type CSSProperties } from 'react' import { IconLogomark } from '@posthog/icons' import { LemonCard } from '@posthog/lemon-ui' @@ -78,6 +78,26 @@ function ExceptionCardContent({ const { currentTab } = useValues(exceptionCardLogic) const { sessionId } = useValues(errorPropertiesLogic) const { setCurrentTab } = useActions(exceptionCardLogic) + const headerRef = useRef(null) + + // Base UI scrolls the active tab into view on mount and on keyboard navigation, but not when the + // value changes from elsewhere. The timeline switches to the recording tab on a timestamp click, so + // in a narrow pane the active tab can sit outside the scroller with no scrollbar to reveal it. + // Nudging scrollLeft rather than calling scrollIntoView keeps ancestors — the page — where they are. + useEffect(() => { + const list = headerRef.current?.querySelector('[data-slot="tabs-list"]') + const active = list?.querySelector('[data-active]') + if (!list || !active) { + return + } + const clippedLeft = active.offsetLeft - list.scrollLeft + const clippedRight = active.offsetLeft + active.offsetWidth - (list.scrollLeft + list.clientWidth) + if (clippedLeft < 0) { + list.scrollLeft += clippedLeft + } else if (clippedRight > 0) { + list.scrollLeft += clippedRight + } + }, [currentTab]) return ( @@ -93,66 +113,72 @@ function ExceptionCardContent({ setCurrentTab(value) } }} - className="@container/exception-card flex min-h-0 flex-1 flex-col gap-0" + className="flex min-h-0 flex-1 flex-col gap-0" > - {/* Container query, not viewport: the card lives in a resizable split pane. Wide — symmetric - 1fr gutters centre the tab bar. Narrow — the gutters shrink to their content and the tab - bar takes the slack; reserving two gutters for one label is what squeezed it. */} -
- {/* overflow-hidden is load-bearing: min-w-0 lets this track collapse, and with nothing - clipping it the label painted straight over the tab bar. */} -
- - {/* sr-only rather than hidden: no room to draw it, but it stays in the a11y tree. */} - Exception -
- {/* Last valve: under ~390px the tab bar alone doesn't fit, so scroll it rather than let - it spill into the actions. justify-start overrides the list's own justify-center — - a centred flex row that overflows spills off *both* edges, putting the first tab - out of reach of the scroller. */} - +
- - Stack Trace - - - Properties - - - Timeline - - + + {/* sr-only rather than hidden: no room to draw it, but it stays in the a11y tree. */} + Exception +
+ {/* Under ~390px the tab bar alone does not fit, so it scrolls. justify-start overrides + the list's own justify-center: a centred flex row that overflows spills off *both* + edges, which puts the first tab out of reach of the scroller. */} + - Recording - - -
- {sessionId && (currentTab === 'timeline' || currentTab === 'recording') ? ( - - ) : null} + + Stack Trace + + + Properties + + + Timeline + + + Recording + + +
+ {sessionId && (currentTab === 'timeline' || currentTab === 'recording') ? ( + + ) : null} +
From 01cfd9cfb8bec9257e37b1c2d3d1c1d30006d5b8 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 11:44:34 +0000 Subject: [PATCH 3/3] chore(visual): update storybook baselines 8 updated Run: 020d378b-5f94-4ae4-969a-912843ab9563 Co-authored-by: cat-ph <268578347+cat-ph@users.noreply.github.com> --- frontend/snapshots.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/frontend/snapshots.yml b/frontend/snapshots.yml index 843b5115bccb..d0ca9c1ec9ff 100644 --- a/frontend/snapshots.yml +++ b/frontend/snapshots.yml @@ -2096,6 +2096,14 @@ snapshots: hash: v1.k794b7964.3c350c23790195a5fc0dfb048f81f0a62dee7d84dab927e4b0fe318adce3443c.uWybQFWQBqnNXQnYT_PSKs0VNv5dVHNtDFIkDAa1Toc errortracking-exceptioncard--exception-card-base--light: hash: v1.k794b7964.40a5fd88410919603dc60b886450c6011806a70502e144dfc559380eec5cd9cf.LXtqQwhm5OiXOmRO3LnQdS-BtoNHpRABg21xA3_FthY + errortracking-exceptioncard--exception-card-header-widths--dark: + hash: v1.k794b7964.a8d4c7c699963c997bbc3683c773912cc14309d04ec5fb79f9ac35e02863f4f6.Zc4-3oyCvK2ZpMaPlKk5YuQ6juFJmcU9-WmDHfxRvYo + errortracking-exceptioncard--exception-card-header-widths--light: + hash: v1.k794b7964.c9d1a5ef4bb29010deec86a6a4b10760ba3e245b819deff4a6b11ce80b8b35e2.NBK6tX4m0khMKb-MRZBfFk87mMQ27hPQ22ZLlzWJtHQ + errortracking-exceptioncard--exception-card-header-widths-with-action--dark: + hash: v1.k794b7964.bcb5832c034ac672244a43ace893fffbb64953dd8f474929937f52ec27f19b14.mAkQ8uO7PW_ilHWHY_oFF4c0LvQcggDt-iLVdnnJRIs + errortracking-exceptioncard--exception-card-header-widths-with-action--light: + hash: v1.k794b7964.25974971fa0cf2dbe86348ce1144e8b8a0ff7c1a7e172ecc941e6b345144841f.dT_74I4ZqX-DFX_ayPWR8lutK4EVque9Zyjbci6Hj6I errortracking-exceptioncard--exception-card-no-in-app--dark: hash: v1.k794b7964.987406687c04214cccecfc03b95f4dcdf9e61a3d708044b579947fb64ed7f33b.nHnR6ColGUC_O2UKa723xfi18hJ3F7EyQCdfEmLKe2I errortracking-exceptioncard--exception-card-no-in-app--light: @@ -2109,9 +2117,9 @@ snapshots: errortracking-exceptioncard--exception-card-no-session-without-steps--light: hash: v1.k794b7964.c52b61b26faf6faf91a760db9c68a35c4c87bd8908e2e0a491d33909c1ef3df0.J9E608EiRs00rwDAHfSM6mRVRJ-ac2oV03bT578xO1w errortracking-exceptioncard--exception-card-session-timeline-with-long-preview-texts--dark: - hash: v1.k794b7964.8051c9343a46757332b4f2dcb5578a9b8e7dc624c206582c1a08f058e187d4d0.sY4-v26oV8Kc_bG78oylfD4r-TGhyHCC7su0PPZAYr4 + hash: v1.k794b7964.8fae23a95b4283fa4852874c783d76403521cc502c10f4720769059b8c18e84d.hVtKsOf2ueGAiMy5UviSuzE8_Vls1D4Gn4O6nzH6IsQ errortracking-exceptioncard--exception-card-session-timeline-with-long-preview-texts--light: - hash: v1.k794b7964.47993b2a28e321edf7f7e4c7db04d76e37bfe39765eb596d560c26f5766a1ad8.Dxc0F54UpFPHBFrXXy3_WmvnqNkWHSfRLbN63VnX1lk + hash: v1.k794b7964.e50270df5056483eb4715a7ffb5e164effe13f62a238f52478fd879183e05dda.QV6rZeHC-Gq341qUb9fq3ozNTDXsbmdt3aZumSizvPg errortracking-exceptioncard--exception-card-session-timeline-with-malformed-steps--dark: hash: v1.k794b7964.83e62f1acebee7145d58a2ce0080c528e4e4076d30b30f912166d1479278e22f.JHO8jnFJrF3WRN4Ae9n5MqjJm8E0oDQrdeM9nDOEw-s errortracking-exceptioncard--exception-card-session-timeline-with-malformed-steps--light: @@ -2125,9 +2133,9 @@ snapshots: errortracking-exceptioncard--exception-card-session-timeline-without-steps--light: hash: v1.k794b7964.638735982d9901d6aa27dbe6affc87c1ec56d3260fa99dc2680045f3b0ec423a.MqdrzbQlSeSod_jU23rgJCkmbQWDowXHv_3Ds68-5Ww errortracking-exceptioncard--exception-card-with-footer--dark: - hash: v1.k794b7964.501916ce06f1ad37217cebd74b549700653ce1ad06deb2087ca813a42a9bfeb2.5UwJuY0r0cQJ3-K6CYskHSqgeKJp1-Fs9R0YH6Vbo7g + hash: v1.k794b7964.d0a0b05ffb6d5261b27a9542196e1bd9e3668d75111bda2026250e6ea635bc0a.MhIZlxFlFhOhy3q_YOS8M974dBnMWlxzyGITaWODRPg errortracking-exceptioncard--exception-card-with-footer--light: - hash: v1.k794b7964.b37a3656195d9cb6c787446c89afac9137f670182826fa035e377ed7ec5cda8a.k2uzwq4sHNjvkn2lNPpOM9ic7aap3sxbozVk8gI5cho + hash: v1.k794b7964.8bc90ffeadb1304543b699edc9997164aa703d7772d00077eeec0aaad813e5cc.jAk-r83tuSk2Izafz9v1wD5Lkdd3tBN0NE8ybyn0Vq0 errortracking-exceptionpropertiestable--default--dark: hash: v1.k794b7964.d3fe66e6fe85d6e4da00ee8f41e6aadd3b056ad0fd6669aa4803e527c6e4244b.WZyU9f9-17LRZgIlwsQcAB5W8UnklBqT0w1YT7_VOwI errortracking-exceptionpropertiestable--default--light: