chore: cleanup properties tab (#34570)

This commit is contained in:
David Newell
2025-07-09 15:51:30 +01:00
committed by GitHub
parent 7a85c5a39f
commit 09f7e45ab2
15 changed files with 199 additions and 187 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -83,7 +83,7 @@ export type FingerprintRecordPart = FingerprintManual | FingerprintFrame | Finge
export interface ExceptionAttributes {
ingestionErrors?: string[]
runtime: ErrorTrackingRuntime
runtime?: ErrorTrackingRuntime
type?: string
value?: string
synthetic?: boolean
@@ -96,7 +96,7 @@ export interface ExceptionAttributes {
sentryUrl?: string
level?: string
url?: string
handled: boolean
handled?: boolean
}
export type SymbolSetStatus = 'valid' | 'invalid'

View File

@@ -17,7 +17,6 @@ import { GenericSelect } from './components/GenericSelect'
import { IssueStatus, StatusIndicator } from './components/Indicator'
import { issueActionsLogic } from './components/IssueActions/issueActionsLogic'
import { errorTrackingIssueSceneLogic } from './errorTrackingIssueSceneLogic'
import { useErrorTagRenderer } from './hooks/use-error-tag-renderer'
import { Metadata } from './issue/Metadata'
import { ISSUE_STATUS_OPTIONS } from './utils'
import { sidePanelLogic } from '~/layout/navigation-3000/sidepanel/sidePanelLogic'
@@ -47,7 +46,7 @@ export function ErrorTrackingIssueScene(): JSX.Element {
useValues(errorTrackingIssueSceneLogic)
const { loadIssue } = useActions(errorTrackingIssueSceneLogic)
const { updateIssueAssignee, updateIssueStatus } = useActions(issueActionsLogic)
const tagRenderer = useErrorTagRenderer()
const hasDiscussions = useFeatureFlag('DISCUSSIONS')
const { openSidePanel } = useActions(sidePanelLogic)
@@ -105,7 +104,6 @@ export function ErrorTrackingIssueScene(): JSX.Element {
issueLoading={issueLoading}
event={selectedEvent ?? undefined}
eventLoading={firstSeenEventLoading}
label={tagRenderer(selectedEvent)}
/>
<ErrorFilters.Root>
<ErrorFilters.DateRange />

View File

@@ -1,9 +1,9 @@
import { LemonCard } from '@posthog/lemon-ui'
import { Meta } from '@storybook/react'
import { getAdditionalProperties, getExceptionAttributes } from 'lib/components/Errors/utils'
import { TEST_EVENTS, TestEventName } from '../__mocks__/events'
import { ContextDisplay, ContextDisplayProps } from './ContextDisplay'
import { getAdditionalProperties, getExceptionAttributes } from 'lib/components/Errors/utils'
const meta: Meta = {
title: 'ErrorTracking/ContextDisplay',
@@ -11,15 +11,6 @@ const meta: Meta = {
layout: 'centered',
viewMode: 'story',
},
decorators: [
(Story: React.FC): JSX.Element => {
return (
<LemonCard hoverEffect={false} className="p-2 px-3 w-[900px]">
<Story />
</LemonCard>
)
},
],
}
export default meta
@@ -27,7 +18,7 @@ export default meta
///////////////////// Context Display
export function ContextDisplayEmpty(): JSX.Element {
return <ContextDisplay loading={false} />
return <ContextDisplay loading={false} exceptionAttributes={{}} additionalProperties={{}} />
}
export function ContextDisplayWithStacktrace(): JSX.Element {
@@ -36,16 +27,12 @@ export function ContextDisplayWithStacktrace(): JSX.Element {
//////////////////// Utils
function getProps(event_name: TestEventName, overrideProps: Record<string, unknown> = {}): ContextDisplayProps {
const properties = event_name ? TEST_EVENTS[event_name].properties : null
const attributes = properties ? getExceptionAttributes(properties) : null
const additionalProperties = properties ? getAdditionalProperties(properties, true) : {}
return {
loading: false,
attributes,
additionalProperties,
...overrideProps,
} as ContextDisplayProps
function getProps(event_name: TestEventName): ContextDisplayProps {
const properties = event_name ? TEST_EVENTS[event_name].properties : {}
const exceptionAttributes = properties ? getExceptionAttributes(properties) : null
const additionalProperties = getAdditionalProperties(properties, true)
return { loading: false, exceptionAttributes, additionalProperties }
}
function ContextWrapperAllEvents({ children }: { children: (props: ContextDisplayProps) => JSX.Element }): JSX.Element {
@@ -55,9 +42,9 @@ function ContextWrapperAllEvents({ children }: { children: (props: ContextDispla
{eventNames.map((name: TestEventName) => {
const props = getProps(name)
return (
<div className="px-3 py-2" key={name}>
<LemonCard hoverEffect={false} className="p-0 w-[900px]">
{children(props)}
</div>
</LemonCard>
)
})}
</div>

View File

@@ -1,120 +1,99 @@
import { IconCopy } from '@posthog/icons'
import { LemonButton, Spinner } from '@posthog/lemon-ui'
import { LemonButton, LemonTable, Spinner } from '@posthog/lemon-ui'
import { ExceptionAttributes } from 'lib/components/Errors/types'
import { concatValues } from 'lib/components/Errors/utils'
import useIsHovering from 'lib/hooks/useIsHovering'
import { identifierToHuman, isObject } from 'lib/utils'
import { identifierToHuman } from 'lib/utils'
import { copyToClipboard } from 'lib/utils/copyToClipboard'
import { cn } from 'lib/utils/css-classes'
import { useRef } from 'react'
import { match } from 'ts-pattern'
import { cancelEvent } from '../utils'
export interface ContextDisplayProps {
className?: string
attributes?: ExceptionAttributes
additionalProperties?: Record<string, unknown>
export type ContextDisplayProps = {
loading: boolean
exceptionAttributes: ExceptionAttributes | null
additionalProperties: Record<string, unknown>
}
export function ContextDisplay({
className,
attributes,
additionalProperties = {},
loading,
exceptionAttributes,
additionalProperties,
}: ContextDisplayProps): JSX.Element {
const additionalEntries = Object.entries(additionalProperties).map(
([key, value]) => [identifierToHuman(key, 'title'), value] as [string, unknown]
)
const exceptionEntries: [string, unknown][] = exceptionAttributes
? [
['Level', exceptionAttributes.level],
['Synthetic', exceptionAttributes.synthetic],
['Library', concatValues(exceptionAttributes, 'lib', 'libVersion')],
['Handled', exceptionAttributes.handled],
['Browser', concatValues(exceptionAttributes, 'browser', 'browserVersion')],
['OS', concatValues(exceptionAttributes, 'os', 'osVersion')],
['URL', exceptionAttributes.url],
]
: []
return (
<div className={className}>
<>
{match(loading)
.with(true, () => (
<div className="flex justify-center w-full h-32 items-center">
<Spinner />
</div>
))
.with(false, () => {
const additionalEntries = Object.entries(additionalProperties).map(
([key, value]) => [identifierToHuman(key, 'title'), value] as [string, unknown]
)
const exceptionEntries =
attributes &&
([
['Level', attributes.level],
['Synthetic', attributes.synthetic],
['Library', concatValues(attributes, 'lib', 'libVersion')],
['Handled', attributes.handled],
['Browser', concatValues(attributes, 'browser', 'browserVersion')],
['OS', concatValues(attributes, 'os', 'osVersion')],
['URL', attributes.url],
] as [string, unknown][])
return (
<div className="space-y-2">
<ContextTable entries={exceptionEntries || []} />
{additionalEntries.length > 0 && <ContextTable entries={additionalEntries} />}
</div>
)
})
.with(false, () => <ContextTable entries={[...exceptionEntries, ...additionalEntries]} />)
.exhaustive()}
</div>
</>
)
}
type ContextRowProps = {
label: string
value: string
}
type ContextTableProps = { entries: [string, unknown][] }
function ContextTable({ entries }: { entries: [string, unknown][] }): JSX.Element {
function ContextTable({ entries }: ContextTableProps): JSX.Element {
return (
<table
className="border-spacing-0 border-separate rounded w-full border overflow-hidden cursor-default"
onClick={cancelEvent}
>
<tbody className="w-full">
{entries
.filter(([, value]) => value !== undefined)
.map(([key, value]) => (
<ContextRow
key={key}
label={key}
value={isObject(value) ? JSON.stringify(value) : String(value)}
/>
))}
{entries.length == 0 && <tr className="w-full text-center">No data available</tr>}
</tbody>
</table>
)
}
function ContextRow({ label, value }: ContextRowProps): JSX.Element {
const valueRef = useRef<HTMLTableCellElement>(null)
const isHovering = useIsHovering(valueRef)
return (
<tr className="even:bg-fill-tertiary w-full group">
<th className="border-r-1 font-semibold text-xs p-1 w-1/3 text-left">{label}</th>
<td ref={valueRef} className="w-full truncate p-1 text-xs max-w-0 relative" title={value}>
{value}
<div
className={cn(
'absolute right-0 top-[50%] translate-y-[-50%] group-even:bg-fill-tertiary group-odd:bg-fill-primary drop-shadow-sm',
isHovering ? 'opacity-100' : 'opacity-0'
)}
>
<LemonButton
size="xsmall"
className={cn('p-0 rounded-none')}
tooltip="Copy"
onClick={() => {
copyToClipboard(value).catch((error) => {
console.error('Failed to copy to clipboard:', error)
})
}}
>
<IconCopy />
</LemonButton>
</div>
</td>
</tr>
<LemonTable
embedded
size="small"
dataSource={entries
.filter(([, value]) => value !== undefined)
.map(([key, value]) => ({
key,
value: String(value),
}))}
showHeader={false}
columns={[
{
title: 'Key',
key: 'key',
dataIndex: 'key',
width: 0,
className: 'font-medium bg-inherit',
render: (dataValue, record) => (
<div className="flex gap-x-2 justify-between items-center">
<div>{dataValue}</div>
<LemonButton
size="xsmall"
tooltip="Copy value"
className="invisible group-hover:visible"
onClick={() =>
copyToClipboard(record.value).catch((error) => {
console.error('Failed to copy to clipboard:', error)
})
}
>
<IconCopy />
</LemonButton>
</div>
),
},
{
title: 'Value',
key: 'value',
dataIndex: 'value',
className: 'whitespace-nowrap',
},
]}
rowClassName="even:bg-fill-tertiary odd:bg-surface-primary group"
firstColumnSticky
/>
)
}

View File

@@ -1,6 +1,6 @@
import { IconLogomark } from '@posthog/icons'
import { LemonCard } from '@posthog/lemon-ui'
import { BindLogic, useActions } from 'kea'
import { BindLogic, useActions, useValues } from 'kea'
import { errorPropertiesLogic, ErrorPropertiesLogicProps } from 'lib/components/Errors/errorPropertiesLogic'
import { ErrorEventType } from 'lib/components/Errors/types'
import { TZLabel } from 'lib/components/TZLabel'
@@ -11,8 +11,11 @@ import { ErrorTrackingRelationalIssue } from '~/queries/schema/schema-general'
import { exceptionCardLogic } from './exceptionCardLogic'
import { PropertiesTab } from './Tabs/PropertiesTab'
import { RawTab } from './Tabs/RawTab'
import { StacktraceTab } from './Tabs/StacktraceTab'
import ViewRecordingTrigger from 'lib/components/ViewRecordingButton/ViewRecordingTrigger'
import { ButtonPrimitive } from 'lib/ui/Button/ButtonPrimitives'
import { match, P } from 'ts-pattern'
import { IconPlayCircle } from 'lib/lemon-ui/icons'
interface ExceptionCardContentProps {
issue?: ErrorTrackingRelationalIssue
@@ -26,7 +29,7 @@ export interface ExceptionCardProps extends Omit<ExceptionCardContentProps, 'tim
eventLoading: boolean
}
export function ExceptionCard({ issue, issueLoading, label, event, eventLoading }: ExceptionCardProps): JSX.Element {
export function ExceptionCard({ issue, issueLoading, event, eventLoading }: ExceptionCardProps): JSX.Element {
const { setLoading } = useActions(exceptionCardLogic)
useEffect(() => {
@@ -43,27 +46,24 @@ export function ExceptionCard({ issue, issueLoading, label, event, eventLoading
} as ErrorPropertiesLogicProps
}
>
<ExceptionCardContent
issue={issue}
label={label}
timestamp={event?.timestamp}
issueLoading={issueLoading}
/>
<ExceptionCardContent issue={issue} timestamp={event?.timestamp} issueLoading={issueLoading} />
</BindLogic>
)
}
function ExceptionCardContent({ issue, issueLoading, timestamp, label }: ExceptionCardContentProps): JSX.Element {
function ExceptionCardContent({ issue, issueLoading, timestamp }: ExceptionCardContentProps): JSX.Element {
const { sessionId, mightHaveRecording } = useValues(errorPropertiesLogic)
return (
<LemonCard hoverEffect={false} className="group p-0 relative overflow-hidden">
<LemonCard hoverEffect={false} className="p-0 relative overflow-hidden">
<TabsPrimitive defaultValue="stacktrace">
<div className="flex justify-between h-[2rem] items-center w-full px-2 border-b">
<TabsPrimitiveList className="flex justify-between w-full h-full items-center">
<div className="w-full h-full">
<TabsPrimitiveTrigger value="raw" className="flex items-center gap-1 text-lg h-full">
<div className="flex items-center gap-1 text-lg h-full">
<IconLogomark />
<span className="text-sm">Exception</span>
</TabsPrimitiveTrigger>
</div>
</div>
<div className="flex gap-2 w-full justify-center h-full">
<TabsPrimitiveTrigger className="px-2" value="stacktrace">
@@ -73,15 +73,32 @@ function ExceptionCardContent({ issue, issueLoading, timestamp, label }: Excepti
Properties
</TabsPrimitiveTrigger>
</div>
<div className="w-full flex gap-2 justify-end items-center">
<div className="w-full flex gap-1 justify-end items-center">
{timestamp && <TZLabel className="text-muted text-xs" time={timestamp} />}
{label}
<ViewRecordingTrigger sessionId={sessionId} inModal={true} timestamp={timestamp}>
{(onClick, _, disabledReason, maybeSpinner) => {
return (
<ButtonPrimitive
disabled={disabledReason != null || !mightHaveRecording}
onClick={onClick}
className="px-2 h-[1.4rem] whitespace-nowrap"
tooltip={match([disabledReason != null, mightHaveRecording])
.with([true, P.any], () => 'No recording available')
.with([false, false], () => 'Recording not ready')
.otherwise(() => 'View Recording')}
>
<IconPlayCircle />
Recording
{maybeSpinner}
</ButtonPrimitive>
)
}}
</ViewRecordingTrigger>
</div>
</TabsPrimitiveList>
</div>
<StacktraceTab value="stacktrace" issue={issue} issueLoading={issueLoading} timestamp={timestamp} />
<PropertiesTab value="properties" />
<RawTab value="raw" />
</TabsPrimitive>
</LemonCard>
)

View File

@@ -1,23 +1,76 @@
import { useValues } from 'kea'
import { errorPropertiesLogic } from 'lib/components/Errors/errorPropertiesLogic'
import { JSONViewer } from 'lib/components/JSONViewer'
import { TabsPrimitiveContent, TabsPrimitiveContentProps } from 'lib/ui/TabsPrimitive/TabsPrimitive'
import { useActions, useValues } from 'kea'
import { ContextDisplay } from '../../ContextDisplay'
import { LemonButton } from '@posthog/lemon-ui'
import { exceptionCardLogic } from '../exceptionCardLogic'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItemIndicator,
DropdownMenuTrigger,
} from 'lib/ui/DropdownMenu/DropdownMenu'
import { ButtonPrimitive } from 'lib/ui/Button/ButtonPrimitives'
import { IconChevronDown } from '@posthog/icons'
import { ContextDisplay } from '../../ContextDisplay'
export interface PropertiesTabProps extends TabsPrimitiveContentProps {}
export function PropertiesTab({ ...props }: PropertiesTabProps): JSX.Element {
const { loading } = useValues(exceptionCardLogic)
const { exceptionAttributes, additionalProperties } = useValues(errorPropertiesLogic)
const { properties, exceptionAttributes, additionalProperties } = useValues(errorPropertiesLogic)
const { loading, showJSONProperties, showAdditionalProperties } = useValues(exceptionCardLogic)
return (
<TabsPrimitiveContent {...props}>
<ContextDisplay
className="w-full p-2"
attributes={exceptionAttributes ?? undefined}
additionalProperties={additionalProperties}
loading={loading}
/>
<div className="flex justify-end items-center border-b-1 bg-surface-secondary">
<ShowDropDownMenu />
</div>
<div>
{showJSONProperties ? (
<JSONViewer src={properties} name="event" collapsed={1} collapseStringsAfterLength={80} sortKeys />
) : (
<ContextDisplay
loading={loading}
exceptionAttributes={exceptionAttributes}
additionalProperties={showAdditionalProperties ? additionalProperties : {}}
/>
)}
</div>
</TabsPrimitiveContent>
)
}
function ShowDropDownMenu(): JSX.Element {
const { showJSONProperties, showAdditionalProperties } = useValues(exceptionCardLogic)
const { setShowJSONProperties, setShowAdditionalProperties } = useActions(exceptionCardLogic)
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<LemonButton size="small" sideIcon={<IconChevronDown />}>
Show
</LemonButton>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuCheckboxItem
checked={showAdditionalProperties}
onCheckedChange={setShowAdditionalProperties}
asChild
>
<ButtonPrimitive menuItem size="sm">
<DropdownMenuItemIndicator intent="checkbox" />
Additional properties
</ButtonPrimitive>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem checked={showJSONProperties} onCheckedChange={setShowJSONProperties} asChild>
<ButtonPrimitive menuItem size="sm">
<DropdownMenuItemIndicator intent="checkbox" />
As JSON
</ButtonPrimitive>
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@@ -1,15 +0,0 @@
import { useValues } from 'kea'
import { errorPropertiesLogic } from 'lib/components/Errors/errorPropertiesLogic'
import { JSONViewer } from 'lib/components/JSONViewer'
import { TabsPrimitiveContent, TabsPrimitiveContentProps } from 'lib/ui/TabsPrimitive/TabsPrimitive'
export interface RawTabProps extends TabsPrimitiveContentProps {}
export function RawTab(props: RawTabProps): JSX.Element {
const { properties } = useValues(errorPropertiesLogic)
return (
<TabsPrimitiveContent className="p-2" {...props}>
<JSONViewer src={properties} name="event" collapsed={1} collapseStringsAfterLength={80} sortKeys />
</TabsPrimitiveContent>
)
}

View File

@@ -3,8 +3,6 @@ import { useActions, useValues } from 'kea'
import { errorPropertiesLogic } from 'lib/components/Errors/errorPropertiesLogic'
import { ExceptionHeaderProps } from 'lib/components/Errors/StackTraces'
import { ErrorTrackingException } from 'lib/components/Errors/types'
import ViewRecordingTrigger from 'lib/components/ViewRecordingButton/ViewRecordingTrigger'
import { IconPlayCircle } from 'lib/lemon-ui/icons'
import { ButtonGroupPrimitive, ButtonPrimitive } from 'lib/ui/Button/ButtonPrimitives'
import {
DropdownMenu,
@@ -24,7 +22,6 @@ import { FixModal } from '../FixModal'
import { StacktraceBaseDisplayProps, StacktraceEmptyDisplay } from '../Stacktrace/StacktraceBase'
import { StacktraceGenericDisplay } from '../Stacktrace/StacktraceGenericDisplay'
import { StacktraceTextDisplay } from '../Stacktrace/StacktraceTextDisplay'
import { match, P } from 'ts-pattern'
export interface StacktraceTabProps extends Omit<TabsPrimitiveContentProps, 'children'> {
issue?: ErrorTrackingRelationalIssue
@@ -40,7 +37,7 @@ export function StacktraceTab({
...props
}: StacktraceTabProps): JSX.Element {
const { loading } = useValues(exceptionCardLogic)
const { exceptionAttributes, exceptionList, sessionId, mightHaveRecording } = useValues(errorPropertiesLogic)
const { exceptionAttributes, exceptionList } = useValues(errorPropertiesLogic)
const showFixButton = hasResolvedStackFrames(exceptionList)
const [showFixModal, setShowFixModal] = useState(false)
return (
@@ -50,25 +47,6 @@ export function StacktraceTab({
<ExceptionAttributesPreview attributes={exceptionAttributes} loading={loading} />
</div>
<ButtonGroupPrimitive size="sm">
<ViewRecordingTrigger sessionId={sessionId} inModal={true} timestamp={timestamp}>
{(onClick, _, disabledReason, maybeSpinner) => {
return (
<ButtonPrimitive
disabled={disabledReason != null || !mightHaveRecording}
onClick={onClick}
className="px-2 h-[1.4rem]"
tooltip={match([disabledReason != null, mightHaveRecording])
.with([true, P.any], () => 'No recording available')
.with([false, false], () => 'Recording not ready')
.otherwise(() => 'View Recording')}
>
<IconPlayCircle />
View Recording
{maybeSpinner}
</ButtonPrimitive>
)
}}
</ViewRecordingTrigger>
{showFixButton && (
<ButtonPrimitive
onClick={() => setShowFixModal(true)}
@@ -76,7 +54,7 @@ export function StacktraceTab({
tooltip="Generate AI prompt to fix this error"
>
<IconMagicWand />
Fix
Fix with AI
</ButtonPrimitive>
)}
<ShowDropDownMenu>
@@ -123,6 +101,7 @@ function StacktraceIssueDisplay({
function ShowDropDownMenu({ children }: { children: React.ReactNode }): JSX.Element {
const { showAllFrames, showAsText } = useValues(exceptionCardLogic)
const { setShowAllFrames, setShowAsText } = useActions(exceptionCardLogic)
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>

View File

@@ -6,16 +6,30 @@ export const exceptionCardLogic = kea<exceptionCardLogicType>([
path(() => ['scenes', 'error-tracking', 'exceptionCardLogic']),
actions({
setShowJSONProperties: (showJSON: boolean) => ({ showJSON }),
setShowAdditionalProperties: (showProperties: boolean) => ({ showProperties }),
setShowAsText: (showAsText: boolean) => ({ showAsText }),
setShowAllFrames: (showAllFrames: boolean) => ({ showAllFrames }),
setLoading: (loading: boolean) => ({ loading }),
}),
reducers({
showJSONProperties: [
false,
{
setShowJSONProperties: (_, { showJSON }) => showJSON,
},
],
showAdditionalProperties: [
false,
{
setShowAdditionalProperties: (_, { showProperties }) => showProperties,
},
],
showAsText: [
false,
{
setShowAsText: (_, { showAsText }: { showAsText: boolean }) => showAsText,
setShowAsText: (_, { showAsText }) => showAsText,
},
],
showAllFrames: [