Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions frontend/snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -66,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 (
<div className="h-[700px] w-full max-w-5xl">
<div className="h-[700px] w-[1000px]">
<ExceptionCard
issueId="issue-id"
issueName="Test Issue"
Expand Down Expand Up @@ -549,8 +553,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')
Expand Down Expand Up @@ -588,3 +592,100 @@ export function ExceptionCardAllEvents(): JSX.Element {
</ExceptionCardWrapperAllEvents>
)
}

//////////////////// Header layout

/*
* The header fits a label, the tab bar and an action into one 40px row. Each width pins a different
* branch of the container query: which parts are drawn, and whether the tab bar centres or scrolls.
*/
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 (
<div className="space-y-6">
{widths.map(({ width, caption }) => (
<div key={width} className="space-y-1">
<div className="text-muted text-xs">{caption}</div>
{/* outline rather than border: a border eats 2px of the card's width and shifts the
container-query boundary these captions name. */}
<div className="h-48 overflow-hidden outline outline-1 outline-[var(--border)]" style={{ width }}>
{children(width)}
</div>
</div>
))}
</div>
)
}

export function ExceptionCardHeaderWidths(): JSX.Element {
const event = asErrorEventType(TEST_EVENTS['javascript_resolved'])

return (
<HeaderWidthMatrix>
{(width) => (
<ExceptionCard issueId={`header-${width}`} issueName="Test Issue" loading={false} event={event} />
)}
</HeaderWidthMatrix>
)
}

/*
* 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 (
<HeaderWidthMatrix widths={HEADER_WIDTHS.filter(({ width }) => width <= 576)}>
{(width) => (
<OpenTimelineTab issueId={`header-action-${width}`}>
<ExceptionCard
issueId={`header-action-${width}`}
issueName="Test Issue"
loading={false}
event={event}
/>
</OpenTimelineTab>
)}
</HeaderWidthMatrix>
)
}
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<string, unknown> {
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'],
},
},
},
},
}
}
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -78,6 +78,26 @@ function ExceptionCardContent({
const { currentTab } = useValues(exceptionCardLogic)
const { sessionId } = useValues(errorPropertiesLogic)
const { setCurrentTab } = useActions(exceptionCardLogic)
const headerRef = useRef<HTMLDivElement>(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<HTMLElement>('[data-slot="tabs-list"]')
const active = list?.querySelector<HTMLElement>('[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 (
<LemonCard hoverEffect={false} className="p-0 relative w-full h-full border-0 rounded-none flex flex-col">
Expand All @@ -95,50 +115,70 @@ function ExceptionCardContent({
}}
className="flex min-h-0 flex-1 flex-col gap-0"
>
<div className="grid h-10 w-full shrink-0 grid-cols-[1fr_auto_1fr] items-center gap-2 border-b px-2">
<div className="flex h-full min-w-0 items-center gap-1 text-lg">
<IconLogomark />
<span className="text-sm">Exception</span>
</div>
<TabsList variant="line" aria-label="Exception details" className="h-full! self-stretch">
<TabsTrigger
value="stack_trace"
className="text-sm"
style={{ '--background': 'transparent' } as CSSProperties}
>
Stack Trace
</TabsTrigger>
<TabsTrigger
value="properties"
className="text-sm"
style={{ '--background': 'transparent' } as CSSProperties}
>
Properties
</TabsTrigger>
<TabsTrigger
value="timeline"
className="text-sm"
style={{ '--background': 'transparent' } as CSSProperties}
>
Timeline
</TabsTrigger>
<TabsTrigger
value="recording"
className="text-sm"
style={{ '--background': 'transparent' } as CSSProperties}
{/* Container query, not viewport: the card sits in a resizable split pane. At @xl and up,
symmetric 1fr gutters centre the tab bar; below it the gutters shrink to their content
so the tab bar keeps the slack. The container wraps the header alone — inline-size
containment zeroes an element's intrinsic width contribution, and on the card as a
whole that collapses it to nothing inside a shrink-to-fit parent. */}
<div className="@container/exception-card shrink-0">
<div
ref={headerRef}
className="grid h-10 w-full grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 border-b px-2 @xl/exception-card:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]"
>
{/* min-w-0 lets this track collapse, so the cell must clip: an unclipped label draws
over the tab bar. */}
<div className="flex h-full min-w-0 items-center gap-1 overflow-hidden text-lg">
<IconLogomark className="shrink-0" />
{/* sr-only rather than hidden: no room to draw it, but it stays in the a11y tree. */}
<span className="truncate text-sm @max-xl/exception-card:sr-only">Exception</span>
</div>
{/* 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. */}
<TabsList
variant="line"
aria-label="Exception details"
className="h-full! max-w-full justify-start self-stretch overflow-x-auto hide-scrollbar"
>
Recording
</TabsTrigger>
</TabsList>
<div className="flex justify-end">
{sessionId && (currentTab === 'timeline' || currentTab === 'recording') ? (
<ViewLogsButton
sessionId={sessionId}
timestamp={timestamp}
size="xsmall"
data-attr="error-tracking-session-view-logs"
/>
) : null}
<TabsTrigger
value="stack_trace"
className="text-sm"
style={{ '--background': 'transparent' } as CSSProperties}
>
Stack Trace
</TabsTrigger>
<TabsTrigger
value="properties"
className="text-sm"
style={{ '--background': 'transparent' } as CSSProperties}
>
Properties
</TabsTrigger>
<TabsTrigger
value="timeline"
className="text-sm"
style={{ '--background': 'transparent' } as CSSProperties}
>
Timeline
</TabsTrigger>
<TabsTrigger
value="recording"
className="text-sm"
style={{ '--background': 'transparent' } as CSSProperties}
>
Recording
</TabsTrigger>
</TabsList>
<div className="flex justify-end">
{sessionId && (currentTab === 'timeline' || currentTab === 'recording') ? (
<ViewLogsButton
sessionId={sessionId}
timestamp={timestamp}
size="xsmall"
data-attr="error-tracking-session-view-logs"
/>
) : null}
</div>
</div>
</div>
<StackTraceTab value="stack_trace" renderActions={renderStackTraceActions} className="flex-1 min-h-0" />
Expand Down
Loading