chore(err): introduce error properties logic (#31769)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
|
Before Width: | Height: | Size: 363 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 349 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 50 KiB |
@@ -149,6 +149,7 @@ export function StacktracelessSafariScriptError(): JSX.Element {
|
||||
eventProperties={errorProperties({
|
||||
$exception_list: [{ type: 'ScriptError', value: 'Script error.', mechanism: { synthetic: true } }],
|
||||
})}
|
||||
eventId="error"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -165,6 +166,7 @@ export function StacktracelessImportModuleError(): JSX.Element {
|
||||
},
|
||||
],
|
||||
})}
|
||||
eventId="error"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -192,6 +194,7 @@ export function AnonymousErrorWithStackTrace(): JSX.Element {
|
||||
},
|
||||
],
|
||||
})}
|
||||
eventId="error"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -246,6 +249,7 @@ export function ChainedErrorStack(): JSX.Element {
|
||||
},
|
||||
],
|
||||
})}
|
||||
eventId="error"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -275,6 +279,7 @@ export function StackTraceWithLineContext(): JSX.Element {
|
||||
},
|
||||
],
|
||||
})}
|
||||
eventId="error"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -291,10 +296,15 @@ export function WithCymbalErrors(): JSX.Element {
|
||||
],
|
||||
$cymbal_errors: ['This is an ingestion error', 'This is a second one'],
|
||||
})}
|
||||
eventId="error"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function SentryStackTrace(): JSX.Element {
|
||||
return <ErrorDisplay eventProperties={errorProperties({ $exception_list: [] })} eventId="error" />
|
||||
}
|
||||
|
||||
export function LegacyEventProperties(): JSX.Element {
|
||||
return (
|
||||
<ErrorDisplay
|
||||
@@ -304,6 +314,7 @@ export function LegacyEventProperties(): JSX.Element {
|
||||
$exception_personURL: 'https://app.posthog.com/person/the-person-id',
|
||||
$exception_synthetic: true,
|
||||
})}
|
||||
eventId="error"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
import { LemonBanner } from '@posthog/lemon-ui'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { BindLogic, useValues } from 'kea'
|
||||
import { TitledSnack } from 'lib/components/TitledSnack'
|
||||
import { LemonDivider } from 'lib/lemon-ui/LemonDivider'
|
||||
import { LemonSwitch } from 'lib/lemon-ui/LemonSwitch'
|
||||
import { Link } from 'lib/lemon-ui/Link'
|
||||
import { getExceptionAttributes } from 'scenes/error-tracking/utils'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { EventType } from '~/types'
|
||||
|
||||
import { FingerprintRecordPart, stackFrameLogic } from './stackFrameLogic'
|
||||
import { errorPropertiesLogic } from './errorPropertiesLogic'
|
||||
import { ChainedStackTraces } from './StackTraces'
|
||||
import { ErrorTrackingException } from './types'
|
||||
import { concatValues, hasInAppFrames, hasStacktrace } from './utils'
|
||||
import { ErrorEventId, ErrorEventProperties } from './types'
|
||||
import { concatValues } from './utils'
|
||||
|
||||
export function ErrorDisplay({ eventProperties }: { eventProperties: EventType['properties'] }): JSX.Element {
|
||||
const exceptionAttributes = getExceptionAttributes(eventProperties)
|
||||
const { type, value, sentryUrl, exceptionList, level, ingestionErrors, handled } = exceptionAttributes
|
||||
|
||||
const exceptionWithStack = hasStacktrace(exceptionList)
|
||||
const fingerprintRecords: FingerprintRecordPart[] = eventProperties.$exception_fingerprint_record || []
|
||||
export function ErrorDisplay({
|
||||
eventProperties,
|
||||
eventId,
|
||||
}: {
|
||||
eventProperties: ErrorEventProperties
|
||||
eventId: ErrorEventId
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<BindLogic logic={errorPropertiesLogic} props={{ properties: eventProperties, id: eventId }}>
|
||||
<ErrorDisplayContent />
|
||||
</BindLogic>
|
||||
)
|
||||
}
|
||||
|
||||
export function ErrorDisplayContent(): JSX.Element {
|
||||
const { exceptionAttributes, hasStacktrace } = useValues(errorPropertiesLogic)
|
||||
const { type, value, sentryUrl, level, ingestionErrors, handled } = exceptionAttributes || {}
|
||||
return (
|
||||
<div className="flex flex-col deprecated-space-y-2 pb-2">
|
||||
<h1 className="mb-0">{type || level}</h1>
|
||||
{!exceptionWithStack && <div className="text-secondary italic">{value}</div>}
|
||||
{!hasStacktrace && <div className="text-secondary italic">{value}</div>}
|
||||
<div className="flex flex-row gap-2 flex-wrap">
|
||||
<TitledSnack
|
||||
type="success"
|
||||
@@ -54,7 +62,7 @@ export function ErrorDisplay({ eventProperties }: { eventProperties: EventType['
|
||||
<TitledSnack title="os" value={concatValues(exceptionAttributes, 'os', 'osVersion') ?? 'unknown'} />
|
||||
</div>
|
||||
|
||||
{ingestionErrors || exceptionWithStack ? <LemonDivider dashed={true} /> : null}
|
||||
{ingestionErrors || hasStacktrace ? <LemonDivider dashed={true} /> : null}
|
||||
{ingestionErrors && (
|
||||
<>
|
||||
<LemonBanner type="error">
|
||||
@@ -66,39 +74,24 @@ export function ErrorDisplay({ eventProperties }: { eventProperties: EventType['
|
||||
</LemonBanner>
|
||||
</>
|
||||
)}
|
||||
{exceptionWithStack && <StackTrace exceptionList={exceptionList} fingerprintRecords={fingerprintRecords} />}
|
||||
{hasStacktrace && <StackTrace />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const StackTrace = ({
|
||||
exceptionList,
|
||||
fingerprintRecords,
|
||||
}: {
|
||||
exceptionList: ErrorTrackingException[]
|
||||
fingerprintRecords: FingerprintRecordPart[]
|
||||
}): JSX.Element => {
|
||||
const { showAllFrames } = useValues(stackFrameLogic)
|
||||
const { setShowAllFrames } = useActions(stackFrameLogic)
|
||||
const hasInApp = hasInAppFrames(exceptionList)
|
||||
|
||||
const StackTrace = (): JSX.Element => {
|
||||
const [showAllFrames, setShowAllFrames] = useState(false)
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-1 mt-6 justify-between items-center">
|
||||
<h3 className="mb-0">Stack Trace</h3>
|
||||
{hasInApp ? (
|
||||
<LemonSwitch
|
||||
checked={showAllFrames}
|
||||
label="Show entire stack trace"
|
||||
onChange={() => setShowAllFrames(!showAllFrames)}
|
||||
/>
|
||||
) : null}
|
||||
<LemonSwitch
|
||||
checked={showAllFrames}
|
||||
label="Show entire stack trace"
|
||||
onChange={() => setShowAllFrames(!showAllFrames)}
|
||||
/>
|
||||
</div>
|
||||
<ChainedStackTraces
|
||||
exceptionList={exceptionList}
|
||||
showAllFrames={hasInApp && showAllFrames}
|
||||
fingerprintRecords={fingerprintRecords}
|
||||
/>
|
||||
<ChainedStackTraces showAllFrames={showAllFrames} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IconFingerprint } from 'lib/lemon-ui/icons'
|
||||
import { cn } from 'lib/utils/css-classes'
|
||||
import { useRef } from 'react'
|
||||
|
||||
import { FingerprintRecordPart } from './stackFrameLogic'
|
||||
import { FingerprintRecordPart } from './types'
|
||||
|
||||
export function FingerprintRecordPartDisplay({
|
||||
part,
|
||||
@@ -16,7 +16,7 @@ export function FingerprintRecordPartDisplay({
|
||||
const iconRef = useRef<HTMLDivElement>(null)
|
||||
const isHovering = useIsHovering(iconRef)
|
||||
return (
|
||||
<Tooltip title={getPartPieces(part)} placement="right">
|
||||
<Tooltip title={renderPartTooltip(part)} placement="right">
|
||||
<span ref={iconRef} className="inline-flex items-center">
|
||||
<IconFingerprint className={className} color={isHovering ? 'red' : 'gray'} fontSize="17px" />
|
||||
</span>
|
||||
@@ -24,7 +24,7 @@ export function FingerprintRecordPartDisplay({
|
||||
)
|
||||
}
|
||||
|
||||
function getPartPieces(component: FingerprintRecordPart): React.ReactNode {
|
||||
function renderPartTooltip(component: FingerprintRecordPart): React.ReactNode {
|
||||
if (component.type === 'manual') {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -5,19 +5,21 @@ import { LemonCollapse, Tooltip } from '@posthog/lemon-ui'
|
||||
import clsx from 'clsx'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag'
|
||||
import { MouseEvent, useEffect, useMemo } from 'react'
|
||||
import { MouseEvent, useEffect } from 'react'
|
||||
import { cancelEvent } from 'scenes/error-tracking/utils'
|
||||
import { match, P } from 'ts-pattern'
|
||||
|
||||
import { CodeLine, getLanguage, Language } from '../CodeSnippet/CodeSnippet'
|
||||
import { CopyToClipboardInline } from '../CopyToClipboard'
|
||||
import { errorPropertiesLogic } from './errorPropertiesLogic'
|
||||
import { FingerprintRecordPartDisplay } from './FingerprintRecordPartDisplay'
|
||||
import { FingerprintRecordPart, stackFrameLogic } from './stackFrameLogic'
|
||||
import { stackFrameLogic } from './stackFrameLogic'
|
||||
import {
|
||||
ErrorTrackingException,
|
||||
ErrorTrackingStackFrame,
|
||||
ErrorTrackingStackFrameContext,
|
||||
ErrorTrackingStackFrameContextLine,
|
||||
FingerprintRecordPart,
|
||||
} from './types'
|
||||
import { stacktraceHasInAppFrames } from './utils'
|
||||
|
||||
@@ -46,22 +48,19 @@ function ExceptionHeader({ type, value, part }: ExceptionHeaderProps): JSX.Eleme
|
||||
type FrameContextClickHandler = (ctx: ErrorTrackingStackFrameContext, e: MouseEvent) => void
|
||||
|
||||
export function ChainedStackTraces({
|
||||
exceptionList,
|
||||
showAllFrames,
|
||||
renderExceptionHeader,
|
||||
onFrameContextClick,
|
||||
embedded = false,
|
||||
fingerprintRecords = [],
|
||||
}: {
|
||||
renderExceptionHeader?: (props: ExceptionHeaderProps) => React.ReactNode
|
||||
exceptionList: ErrorTrackingException[]
|
||||
fingerprintRecords?: FingerprintRecordPart[]
|
||||
showAllFrames: boolean
|
||||
embedded?: boolean
|
||||
onFrameContextClick?: FrameContextClickHandler
|
||||
}): JSX.Element {
|
||||
const { loadFromRawIds } = useActions(stackFrameLogic)
|
||||
const getters = useFingerprintGetters(fingerprintRecords)
|
||||
const { exceptionList, getExceptionFingerprint } = useValues(errorPropertiesLogic)
|
||||
|
||||
useEffect(() => {
|
||||
const frames: ErrorTrackingStackFrame[] = exceptionList.flatMap((e) => {
|
||||
@@ -78,7 +77,7 @@ export function ChainedStackTraces({
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{exceptionList.map(({ stacktrace, value, type, id }, index) => {
|
||||
const displayTrace = shouldDisplayTrace(stacktrace, showAllFrames)
|
||||
const part = getters.getExceptionPart(id)
|
||||
const part = getExceptionFingerprint(id)
|
||||
const traceHeaderProps = { id, type, value, part, loading: false }
|
||||
return (
|
||||
<div
|
||||
@@ -94,7 +93,6 @@ export function ChainedStackTraces({
|
||||
frames={stacktrace?.frames || []}
|
||||
showAllFrames={showAllFrames}
|
||||
embedded={embedded}
|
||||
getters={getters}
|
||||
onFrameContextClick={onFrameContextClick}
|
||||
/>
|
||||
)}
|
||||
@@ -123,13 +121,11 @@ function Trace({
|
||||
frames,
|
||||
showAllFrames,
|
||||
embedded,
|
||||
getters,
|
||||
onFrameContextClick,
|
||||
}: {
|
||||
frames: ErrorTrackingStackFrame[]
|
||||
showAllFrames: boolean
|
||||
embedded: boolean
|
||||
getters?: FingerprintGetters
|
||||
onFrameContextClick?: FrameContextClickHandler
|
||||
}): JSX.Element | null {
|
||||
const { stackFrameRecords } = useValues(stackFrameLogic)
|
||||
@@ -140,7 +136,7 @@ function Trace({
|
||||
const record = stackFrameRecords[raw_id]
|
||||
return {
|
||||
key: idx,
|
||||
header: <FrameHeaderDisplay frame={frame} getters={getters} />,
|
||||
header: <FrameHeaderDisplay frame={frame} />,
|
||||
content:
|
||||
record && record.context ? (
|
||||
<div onClick={(e) => onFrameContextClick?.(record.context!, e)}>
|
||||
@@ -154,15 +150,10 @@ function Trace({
|
||||
return <LemonCollapse embedded={embedded} multiple panels={panels} size="xsmall" />
|
||||
}
|
||||
|
||||
export function FrameHeaderDisplay({
|
||||
frame,
|
||||
getters,
|
||||
}: {
|
||||
frame: ErrorTrackingStackFrame
|
||||
getters?: FingerprintGetters
|
||||
}): JSX.Element {
|
||||
export function FrameHeaderDisplay({ frame }: { frame: ErrorTrackingStackFrame }): JSX.Element {
|
||||
const { raw_id, source, line, column, resolved_name, resolved, resolve_failure, in_app } = frame
|
||||
const part = getters?.getFramePart(raw_id)
|
||||
const { getFrameFingerprint } = useValues(errorPropertiesLogic)
|
||||
const part = getFrameFingerprint(raw_id)
|
||||
return (
|
||||
<div className="flex flex-1 justify-between items-center h-full">
|
||||
<div className="flex flex-wrap gap-x-1">
|
||||
@@ -210,24 +201,6 @@ export function FrameHeaderDisplay({
|
||||
)
|
||||
}
|
||||
|
||||
export type FingerprintGetters = {
|
||||
getExceptionPart(excId: string): FingerprintRecordPart | undefined
|
||||
getFramePart(frameId: string): FingerprintRecordPart | undefined
|
||||
}
|
||||
|
||||
function useFingerprintGetters(fingerprintRecords: FingerprintRecordPart[]): FingerprintGetters {
|
||||
return useMemo(() => {
|
||||
return {
|
||||
getExceptionPart(excId: string) {
|
||||
return fingerprintRecords.find((record) => record.type === 'exception' && record.id === excId)
|
||||
},
|
||||
getFramePart(frameId: string) {
|
||||
return fingerprintRecords.find((record) => record.type === 'frame' && record.raw_id === frameId)
|
||||
},
|
||||
}
|
||||
}, [fingerprintRecords])
|
||||
}
|
||||
|
||||
function FrameContext({
|
||||
context,
|
||||
language,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ExceptionAttributes, getExceptionAttributes } from 'scenes/error-tracking/utils'
|
||||
import { ExceptionAttributes } from './types'
|
||||
import { getExceptionAttributes, getExceptionList } from './utils'
|
||||
|
||||
describe('Error Display', () => {
|
||||
it('can read sentry stack trace when $exception_list is not present', () => {
|
||||
@@ -47,47 +48,28 @@ describe('Error Display', () => {
|
||||
$exception_personURL: 'https://app.posthog.com/person/f6kW3HXaha6dAvHZiOmgrcAXK09682P6nNPxvfjqM9c',
|
||||
$exception_type: 'Error',
|
||||
}
|
||||
const result = getExceptionAttributes(eventProperties)
|
||||
expect(result).toEqual({
|
||||
browser: 'Chrome',
|
||||
browserVersion: '92.0.4515',
|
||||
value: 'There was an error creating the support ticket with zendesk.',
|
||||
exceptionList: [
|
||||
{
|
||||
mechanism: {
|
||||
handled: true,
|
||||
type: 'generic',
|
||||
},
|
||||
stacktrace: {
|
||||
frames: [
|
||||
{
|
||||
colno: 220,
|
||||
filename: 'https://app-static-prod.posthog.com/static/chunk-UFQKIDIH.js',
|
||||
function: 'submitZendeskTicket',
|
||||
in_app: true,
|
||||
lineno: 25,
|
||||
},
|
||||
],
|
||||
},
|
||||
type: 'Error',
|
||||
value: 'There was an error creating the support ticket with zendesk.',
|
||||
const result = getExceptionList(eventProperties)
|
||||
expect(result).toEqual([
|
||||
{
|
||||
mechanism: {
|
||||
handled: true,
|
||||
type: 'generic',
|
||||
},
|
||||
],
|
||||
synthetic: undefined,
|
||||
handled: true,
|
||||
type: 'Error',
|
||||
lib: 'posthog-js',
|
||||
libVersion: '1.0.0',
|
||||
level: undefined,
|
||||
os: 'Windows',
|
||||
osVersion: '10',
|
||||
ingestionErrors: undefined,
|
||||
fingerprintRecords: [],
|
||||
url: undefined,
|
||||
runtime: 'web',
|
||||
sentryUrl:
|
||||
'https://sentry.io/organizations/posthog/issues/?project=1899813&query=40e442d79c22473391aeeeba54c82163',
|
||||
})
|
||||
stacktrace: {
|
||||
frames: [
|
||||
{
|
||||
colno: 220,
|
||||
filename: 'https://app-static-prod.posthog.com/static/chunk-UFQKIDIH.js',
|
||||
function: 'submitZendeskTicket',
|
||||
in_app: true,
|
||||
lineno: 25,
|
||||
},
|
||||
],
|
||||
},
|
||||
type: 'Error',
|
||||
value: 'There was an error creating the support ticket with zendesk.',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('can read sentry message', () => {
|
||||
@@ -119,12 +101,10 @@ describe('Error Display', () => {
|
||||
browser: 'Chrome',
|
||||
browserVersion: '92.0.4515',
|
||||
value: 'the message sent into sentry captureMessage',
|
||||
exceptionList: [],
|
||||
ingestionErrors: undefined,
|
||||
handled: false,
|
||||
synthetic: undefined,
|
||||
type: undefined,
|
||||
fingerprintRecords: [],
|
||||
url: undefined,
|
||||
runtime: 'web',
|
||||
lib: 'posthog-js',
|
||||
@@ -183,32 +163,9 @@ describe('Error Display', () => {
|
||||
level: undefined,
|
||||
os: 'Windows',
|
||||
osVersion: '10',
|
||||
fingerprintRecords: [],
|
||||
url: undefined,
|
||||
runtime: 'web',
|
||||
sentryUrl: undefined,
|
||||
exceptionList: [
|
||||
{
|
||||
mechanism: {
|
||||
handled: true,
|
||||
type: 'generic',
|
||||
synthetic: false,
|
||||
},
|
||||
stacktrace: {
|
||||
frames: [
|
||||
{
|
||||
colno: 220,
|
||||
filename: 'https://app-static-prod.posthog.com/static/chunk-UFQKIDIH.js',
|
||||
function: 'submitZendeskTicket',
|
||||
in_app: true,
|
||||
lineno: 25,
|
||||
},
|
||||
],
|
||||
},
|
||||
type: 'Error',
|
||||
value: 'There was an error creating the support ticket with zendesk2.',
|
||||
},
|
||||
],
|
||||
ingestionErrors: undefined,
|
||||
handled: true,
|
||||
})
|
||||
|
||||
81
frontend/src/lib/components/Errors/errorPropertiesLogic.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { connect, kea, key, path, props, selectors } from 'kea'
|
||||
import {
|
||||
ErrorEventId,
|
||||
ErrorEventProperties,
|
||||
ErrorTrackingException,
|
||||
FingerprintRecordPart,
|
||||
} from 'lib/components/Errors/types'
|
||||
import {
|
||||
getAdditionalProperties,
|
||||
getExceptionAttributes,
|
||||
getExceptionList,
|
||||
getFingerprintRecords,
|
||||
getSessionId,
|
||||
hasStacktrace,
|
||||
} from 'lib/components/Errors/utils'
|
||||
import { dayjs } from 'lib/dayjs'
|
||||
import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic'
|
||||
|
||||
import type { errorPropertiesLogicType } from './errorPropertiesLogicType'
|
||||
|
||||
export interface ErrorPropertiesLogicProps {
|
||||
properties?: ErrorEventProperties
|
||||
id: ErrorEventId
|
||||
}
|
||||
|
||||
export const errorPropertiesLogic = kea<errorPropertiesLogicType>([
|
||||
path((key) => ['components', 'Errors', 'errorPropertiesLogic', key]),
|
||||
props({} as ErrorPropertiesLogicProps),
|
||||
key((props) => props.id),
|
||||
|
||||
connect(() => ({
|
||||
values: [preflightLogic, ['isCloudOrDev']],
|
||||
})),
|
||||
|
||||
selectors({
|
||||
properties: [
|
||||
() => [(_, props) => props.properties as ErrorEventProperties],
|
||||
(properties: ErrorEventProperties) => properties,
|
||||
],
|
||||
exceptionAttributes: [
|
||||
(s) => [s.properties],
|
||||
(properties: ErrorEventProperties) => (properties ? getExceptionAttributes(properties) : null),
|
||||
],
|
||||
exceptionList: [
|
||||
(s) => [s.properties],
|
||||
(properties: ErrorEventProperties) => {
|
||||
return properties ? getExceptionList(properties) : []
|
||||
},
|
||||
],
|
||||
timestamp: [
|
||||
(s) => [s.properties],
|
||||
(properties: ErrorEventProperties) => {
|
||||
return properties ? dayjs(properties.timestamp as string) : null
|
||||
},
|
||||
],
|
||||
additionalProperties: [
|
||||
(s) => [s.properties, s.isCloudOrDev],
|
||||
(properties: ErrorEventProperties, isCloudOrDev: boolean | undefined) =>
|
||||
properties ? getAdditionalProperties(properties, isCloudOrDev) : {},
|
||||
],
|
||||
fingerprintRecords: [
|
||||
(s) => [s.properties],
|
||||
(properties: ErrorEventProperties) => (properties ? getFingerprintRecords(properties) : []),
|
||||
],
|
||||
hasStacktrace: [(s) => [s.exceptionList], (excList: ErrorTrackingException[]) => hasStacktrace(excList)],
|
||||
sessionId: [
|
||||
(s) => [s.properties],
|
||||
(properties: ErrorEventProperties) => (properties ? getSessionId(properties) : undefined),
|
||||
],
|
||||
getExceptionFingerprint: [
|
||||
(s) => [s.fingerprintRecords],
|
||||
(records: FingerprintRecordPart[]) => (excId: string) =>
|
||||
records.find((record) => record.type === 'exception' && record.id === excId),
|
||||
],
|
||||
getFrameFingerprint: [
|
||||
(s) => [s.fingerprintRecords],
|
||||
(records: FingerprintRecordPart[]) => (frameRawId: string) =>
|
||||
records.find((record) => record.type === 'frame' && record.raw_id === frameRawId),
|
||||
],
|
||||
}),
|
||||
])
|
||||
@@ -1,4 +1,4 @@
|
||||
import { actions, kea, path, reducers } from 'kea'
|
||||
import { actions, kea, path } from 'kea'
|
||||
import { loaders } from 'kea-loaders'
|
||||
import api from 'lib/api'
|
||||
|
||||
@@ -14,51 +14,14 @@ function mapStackFrameRecords(
|
||||
return newRecords.reduce((frames, record) => ({ ...frames, [record.raw_id]: record }), initialRecords)
|
||||
}
|
||||
|
||||
interface FingerprintFrame {
|
||||
type: 'frame'
|
||||
raw_id: string
|
||||
pieces: string[]
|
||||
}
|
||||
|
||||
interface FingerprintException {
|
||||
type: 'exception'
|
||||
id: string // Exception ID
|
||||
pieces: string[]
|
||||
}
|
||||
|
||||
interface FingerprintManual {
|
||||
type: 'manual'
|
||||
}
|
||||
|
||||
export type FingerprintRecordPart = FingerprintManual | FingerprintFrame | FingerprintException
|
||||
|
||||
export const stackFrameLogic = kea<stackFrameLogicType>([
|
||||
path(['components', 'Errors', 'stackFrameLogic']),
|
||||
|
||||
actions({
|
||||
loadFromRawIds: (rawIds: ErrorTrackingStackFrame['raw_id'][]) => ({ rawIds }),
|
||||
loadForSymbolSet: (symbolSetId: ErrorTrackingSymbolSet['id']) => ({ symbolSetId }),
|
||||
setShowAllFrames: (showAllFrames: boolean) => ({ showAllFrames }),
|
||||
setFrameOrderReversed: (reverseOrder: boolean) => ({ reverseOrder }),
|
||||
}),
|
||||
|
||||
reducers(() => ({
|
||||
showAllFrames: [
|
||||
false,
|
||||
{ persist: true },
|
||||
{
|
||||
setShowAllFrames: (_, { showAllFrames }) => showAllFrames,
|
||||
},
|
||||
],
|
||||
frameOrderReversed: [
|
||||
false,
|
||||
{ persist: true },
|
||||
{
|
||||
setFrameOrderReversed: (_, { reverseOrder }) => reverseOrder,
|
||||
},
|
||||
],
|
||||
})),
|
||||
|
||||
loaders(({ values }) => ({
|
||||
stackFrameRecords: [
|
||||
{} as KeyedStackFrameRecords,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { EventType } from '~/types'
|
||||
|
||||
export interface ErrorTrackingException {
|
||||
stacktrace?: ErrorTrackingRawStackTrace | ErrorTrackingResolvedStackTrace
|
||||
module: string
|
||||
@@ -61,5 +63,43 @@ export interface ErrorTrackingSymbolSet {
|
||||
failure_reason: string | null
|
||||
}
|
||||
|
||||
interface FingerprintFrame {
|
||||
type: 'frame'
|
||||
raw_id: string
|
||||
pieces: string[]
|
||||
}
|
||||
|
||||
interface FingerprintException {
|
||||
type: 'exception'
|
||||
id: string // Exception ID
|
||||
pieces: string[]
|
||||
}
|
||||
|
||||
interface FingerprintManual {
|
||||
type: 'manual'
|
||||
}
|
||||
|
||||
export type FingerprintRecordPart = FingerprintManual | FingerprintFrame | FingerprintException
|
||||
|
||||
export interface ExceptionAttributes {
|
||||
ingestionErrors?: string[]
|
||||
runtime: ErrorTrackingRuntime
|
||||
type?: string
|
||||
value?: string
|
||||
synthetic?: boolean
|
||||
lib?: string
|
||||
libVersion?: string
|
||||
browser?: string
|
||||
browserVersion?: string
|
||||
os?: string
|
||||
osVersion?: string
|
||||
sentryUrl?: string
|
||||
level?: string
|
||||
url?: string
|
||||
handled: boolean
|
||||
}
|
||||
|
||||
export type SymbolSetStatus = 'valid' | 'invalid'
|
||||
export type SymbolSetStatusFilter = SymbolSetStatus | 'all'
|
||||
export type ErrorEventProperties = EventType['properties']
|
||||
export type ErrorEventId = NonNullable<EventType['uuid']>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { ExceptionAttributes } from 'scenes/error-tracking/utils'
|
||||
import { isPostHogProperty } from '~/taxonomy/taxonomy'
|
||||
|
||||
import { ErrorTrackingException, ErrorTrackingRuntime } from './types'
|
||||
import {
|
||||
ErrorEventProperties,
|
||||
ErrorTrackingException,
|
||||
ErrorTrackingRuntime,
|
||||
ExceptionAttributes,
|
||||
FingerprintRecordPart,
|
||||
} from './types'
|
||||
|
||||
export function hasStacktrace(exceptionList: ErrorTrackingException[]): boolean {
|
||||
return exceptionList.length > 0 && exceptionList.some((e) => !!e.stacktrace)
|
||||
@@ -41,3 +47,86 @@ export function concatValues(
|
||||
}
|
||||
return definedKeys.map((key) => attrs[key]).join(' ')
|
||||
}
|
||||
|
||||
export function getExceptionAttributes(properties: Record<string, any>): ExceptionAttributes {
|
||||
const {
|
||||
$lib: lib,
|
||||
$lib_version: libVersion,
|
||||
$browser: browser,
|
||||
$browser_version: browserVersion,
|
||||
$os: os,
|
||||
$os_version: osVersion,
|
||||
$sentry_url: sentryUrl,
|
||||
$level: level,
|
||||
$cymbal_errors: ingestionErrors,
|
||||
} = properties
|
||||
|
||||
let type = properties.$exception_type
|
||||
let value = properties.$exception_message
|
||||
let synthetic: boolean | undefined = properties.$exception_synthetic
|
||||
const url: string | undefined = properties.$current_url
|
||||
const exceptionList: ErrorTrackingException[] | undefined = getExceptionList(properties)
|
||||
if (!type) {
|
||||
type = exceptionList?.[0]?.type
|
||||
}
|
||||
if (!value) {
|
||||
value = exceptionList?.[0]?.value
|
||||
}
|
||||
if (synthetic == undefined) {
|
||||
synthetic = exceptionList?.[0]?.mechanism?.synthetic
|
||||
}
|
||||
|
||||
const handled = exceptionList?.[0]?.mechanism?.handled ?? false
|
||||
const runtime: ErrorTrackingRuntime = getRuntimeFromLib(lib)
|
||||
|
||||
return {
|
||||
type,
|
||||
value,
|
||||
synthetic,
|
||||
runtime,
|
||||
lib,
|
||||
libVersion,
|
||||
browser,
|
||||
browserVersion,
|
||||
os,
|
||||
osVersion,
|
||||
url,
|
||||
sentryUrl,
|
||||
handled,
|
||||
level,
|
||||
ingestionErrors,
|
||||
}
|
||||
}
|
||||
|
||||
export function getExceptionList(properties: ErrorEventProperties): ErrorTrackingException[] {
|
||||
const { $sentry_exception } = properties
|
||||
let exceptionList: ErrorTrackingException[] | undefined = properties.$exception_list
|
||||
// exception autocapture sets $exception_list for all exceptions.
|
||||
// If it's not present, then this is probably a sentry exception. Get this list from the sentry_exception
|
||||
if (!exceptionList?.length && $sentry_exception) {
|
||||
if (Array.isArray($sentry_exception.values)) {
|
||||
exceptionList = $sentry_exception.values
|
||||
}
|
||||
}
|
||||
return exceptionList || []
|
||||
}
|
||||
|
||||
export function getFingerprintRecords(properties: ErrorEventProperties): FingerprintRecordPart[] {
|
||||
const { $exception_fingerprint_record } = properties
|
||||
return $exception_fingerprint_record || []
|
||||
}
|
||||
|
||||
export function getAdditionalProperties(
|
||||
properties: ErrorEventProperties,
|
||||
isCloudOrDev: boolean | undefined
|
||||
): Record<string, unknown> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(properties).filter(([key]) => {
|
||||
return !isPostHogProperty(key, isCloudOrDev)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export function getSessionId(properties: ErrorEventProperties): string | undefined {
|
||||
return properties['$session_id'] as string | undefined
|
||||
}
|
||||
|
||||
@@ -142,7 +142,11 @@ export function EventDetails({ event, tableProps }: EventDetailsProps): JSX.Elem
|
||||
label: 'Exception',
|
||||
content: (
|
||||
<div className="mx-3">
|
||||
<ErrorDisplay eventProperties={event.properties} />
|
||||
<ErrorDisplay
|
||||
eventProperties={event.properties}
|
||||
// fallback on timestamp as uuid is optional
|
||||
eventId={event.uuid ?? event.timestamp ?? 'error'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ErrorTrackingIssue } from '~/queries/schema/schema-general'
|
||||
|
||||
import { AssigneeIconDisplay, AssigneeLabelDisplay } from './components/Assignee/AssigneeDisplay'
|
||||
import { AssigneeSelect } from './components/Assignee/AssigneeSelect'
|
||||
import { IssueCard } from './components/IssueCard'
|
||||
import { ExceptionCard } from './components/ExceptionCard'
|
||||
import { DateRangeFilter, FilterGroup, InternalAccountsFilter } from './ErrorTrackingFilters'
|
||||
import { errorTrackingIssueSceneLogic } from './errorTrackingIssueSceneLogic'
|
||||
import { ErrorTrackingSetupPrompt } from './ErrorTrackingSetupPrompt'
|
||||
@@ -37,7 +37,7 @@ export const STATUS_LABEL: Record<ErrorTrackingIssue['status'], string> = {
|
||||
}
|
||||
|
||||
export function ErrorTrackingIssueScene(): JSX.Element {
|
||||
const { issue, issueLoading } = useValues(errorTrackingIssueSceneLogic)
|
||||
const { issue, issueLoading, properties, propertiesLoading } = useValues(errorTrackingIssueSceneLogic)
|
||||
const { loadIssue, updateStatus, updateAssignee } = useActions(errorTrackingIssueSceneLogic)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -77,7 +77,12 @@ export function ErrorTrackingIssueScene(): JSX.Element {
|
||||
}
|
||||
/>
|
||||
<div className="ErrorTrackingIssue space-y-2">
|
||||
<IssueCard />
|
||||
<ExceptionCard
|
||||
issue={issue ?? undefined}
|
||||
issueLoading={issueLoading}
|
||||
properties={properties ?? undefined}
|
||||
propertiesLoading={propertiesLoading}
|
||||
/>
|
||||
<div className="flex items-center gap-2 p-0 bg-transparent">
|
||||
<div className="h-full flex items-center justify-center w-full gap-2">
|
||||
<DateRangeFilter />
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import javascript_empty from './javascript_empty.json'
|
||||
import javascript_resolved from './javascript_resolved.json'
|
||||
import javascript_unresolved from './javascript_unresolved.json'
|
||||
import node_unresolved from './node_unresolved.json'
|
||||
import python_multierror from './python_multierror.json'
|
||||
import python_resolved from './python_resolved.json'
|
||||
|
||||
export const TEST_EVENTS = {
|
||||
javascript_empty,
|
||||
javascript_resolved,
|
||||
javascript_unresolved,
|
||||
node_unresolved,
|
||||
python_resolved,
|
||||
python_multierror,
|
||||
}
|
||||
|
||||
export type TestEventNames = keyof typeof TEST_EVENTS
|
||||
@@ -0,0 +1,45 @@
|
||||
import { BindLogic } from 'kea'
|
||||
import { errorPropertiesLogic } from 'lib/components/Errors/errorPropertiesLogic'
|
||||
import { ErrorEventProperties } from 'lib/components/Errors/types'
|
||||
import { exceptionCardLogic } from 'scenes/error-tracking/components/ExceptionCard/exceptionCardLogic'
|
||||
|
||||
import javascript_empty from './javascript_empty.json'
|
||||
import javascript_resolved from './javascript_resolved.json'
|
||||
import javascript_unresolved from './javascript_unresolved.json'
|
||||
import node_unresolved from './node_unresolved.json'
|
||||
import python_multierror from './python_multierror.json'
|
||||
import python_resolved from './python_resolved.json'
|
||||
|
||||
export const TEST_EVENTS = {
|
||||
javascript_empty,
|
||||
javascript_resolved,
|
||||
javascript_unresolved,
|
||||
node_unresolved,
|
||||
python_resolved,
|
||||
python_multierror,
|
||||
}
|
||||
|
||||
export type TestEventName = keyof typeof TEST_EVENTS
|
||||
|
||||
export function getEventProperties(eventName: TestEventName): ErrorEventProperties {
|
||||
return TEST_EVENTS[eventName].properties
|
||||
}
|
||||
|
||||
export function ExceptionLogicWrapper({
|
||||
eventName,
|
||||
loading = false,
|
||||
children,
|
||||
}: {
|
||||
eventName: TestEventName
|
||||
loading?: boolean
|
||||
children: JSX.Element
|
||||
}): JSX.Element {
|
||||
const properties = getEventProperties(eventName)
|
||||
return (
|
||||
<BindLogic logic={exceptionCardLogic} props={{ loading }}>
|
||||
<BindLogic logic={errorPropertiesLogic} props={{ properties: properties, id: eventName }}>
|
||||
{children}
|
||||
</BindLogic>
|
||||
</BindLogic>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LemonCard } from '@posthog/lemon-ui'
|
||||
import { Meta } from '@storybook/react'
|
||||
import { TEST_EVENTS, TestEventNames } from 'scenes/error-tracking/__mocks__/events'
|
||||
import { getAdditionalProperties, getExceptionAttributes } from 'scenes/error-tracking/utils'
|
||||
import { getAdditionalProperties, getExceptionAttributes } from 'lib/components/Errors/utils'
|
||||
import { TEST_EVENTS, TestEventName } from 'scenes/error-tracking/__mocks__/events'
|
||||
|
||||
import { ContextDisplay, ContextDisplayProps } from './ContextDisplay'
|
||||
|
||||
@@ -27,7 +27,7 @@ export default meta
|
||||
///////////////////// Context Display
|
||||
|
||||
export function ContextDisplayEmpty(): JSX.Element {
|
||||
return <ContextDisplay attributes={null} additionalProperties={{}} loading={false} />
|
||||
return <ContextDisplay loading={false} />
|
||||
}
|
||||
|
||||
export function ContextDisplayWithStacktrace(): JSX.Element {
|
||||
@@ -36,7 +36,7 @@ export function ContextDisplayWithStacktrace(): JSX.Element {
|
||||
|
||||
//////////////////// Utils
|
||||
|
||||
function getProps(event_name: TestEventNames | null, overrideProps: Record<string, unknown> = {}): ContextDisplayProps {
|
||||
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) : {}
|
||||
@@ -49,10 +49,10 @@ function getProps(event_name: TestEventNames | null, overrideProps: Record<strin
|
||||
}
|
||||
|
||||
function ContextWrapperAllEvents({ children }: { children: (props: ContextDisplayProps) => JSX.Element }): JSX.Element {
|
||||
const eventNames = Object.keys(TEST_EVENTS) as TestEventNames[]
|
||||
const eventNames = Object.keys(TEST_EVENTS) as TestEventName[]
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{eventNames.map((name: TestEventNames) => {
|
||||
{eventNames.map((name: TestEventName) => {
|
||||
const props = getProps(name)
|
||||
return (
|
||||
<div className="px-3 py-2" key={name}>
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { IconCopy } from '@posthog/icons'
|
||||
import { LemonButton, 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 } from 'lib/utils'
|
||||
import { copyToClipboard } from 'lib/utils/copyToClipboard'
|
||||
import { cn } from 'lib/utils/css-classes'
|
||||
import { Properties } from 'posthog-js'
|
||||
import { useRef } from 'react'
|
||||
import { match } from 'ts-pattern'
|
||||
|
||||
import { cancelEvent, ExceptionAttributes } from '../utils'
|
||||
import { cancelEvent } from '../utils'
|
||||
|
||||
export interface ContextDisplayProps {
|
||||
className?: string
|
||||
attributes: ExceptionAttributes | null
|
||||
additionalProperties: Properties
|
||||
attributes?: ExceptionAttributes
|
||||
additionalProperties?: Record<string, unknown>
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export function ContextDisplay({
|
||||
className,
|
||||
attributes,
|
||||
additionalProperties,
|
||||
additionalProperties = {},
|
||||
loading,
|
||||
}: ContextDisplayProps): JSX.Element {
|
||||
return (
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { IconBug } from '@posthog/icons'
|
||||
import { LemonTag } from '@posthog/lemon-ui'
|
||||
import { PropertyIcon } from 'lib/components/PropertyIcon/PropertyIcon'
|
||||
import { Children } from 'react'
|
||||
import { ExceptionAttributes } from 'scenes/error-tracking/utils'
|
||||
|
||||
export interface ExceptionAttributesIconListProps {
|
||||
attributes: ExceptionAttributes
|
||||
}
|
||||
|
||||
export function ExceptionAttributesIconList({ attributes }: ExceptionAttributesIconListProps): JSX.Element {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<PropertyWrapper title="Unhandled" visible={!attributes.handled}>
|
||||
<IconBug className="text-sm text-secondary" />
|
||||
</PropertyWrapper>
|
||||
<PropertyWrapper title={attributes.browser} visible={!!attributes.browser}>
|
||||
<PropertyIcon property="$browser" value={attributes.browser} className="text-sm text-secondary" />
|
||||
</PropertyWrapper>
|
||||
<PropertyWrapper title={attributes.os} visible={!!attributes.os}>
|
||||
<PropertyIcon property="$os" value={attributes.os} className="text-sm text-secondary" />
|
||||
</PropertyWrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function PropertyWrapper({
|
||||
title,
|
||||
visible = true,
|
||||
children,
|
||||
}: {
|
||||
title?: string
|
||||
visible?: boolean
|
||||
children: JSX.Element
|
||||
}): JSX.Element {
|
||||
if (Children.count(children) == 0 || title === undefined || !visible) {
|
||||
return <></>
|
||||
}
|
||||
return (
|
||||
<LemonTag>
|
||||
{children}
|
||||
<span className="capitalize">{title}</span>
|
||||
</LemonTag>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { IconBug } from '@posthog/icons'
|
||||
import { LemonTag, Spinner } from '@posthog/lemon-ui'
|
||||
import { ExceptionAttributes } from 'lib/components/Errors/types'
|
||||
import { PropertyIcon } from 'lib/components/PropertyIcon/PropertyIcon'
|
||||
import { Children } from 'react'
|
||||
import { match } from 'ts-pattern'
|
||||
|
||||
export interface ExceptionAttributesPreviewProps {
|
||||
attributes: ExceptionAttributes | null
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export function ExceptionAttributesPreview({ attributes, loading }: ExceptionAttributesPreviewProps): JSX.Element {
|
||||
return (
|
||||
<span className="flex items-center gap-1 text-muted group-hover:text-brand-red">
|
||||
{match(loading)
|
||||
.with(true, () => (
|
||||
<span className="text-muted space-x-2 text-xs">
|
||||
<Spinner />
|
||||
<span>Loading details...</span>
|
||||
</span>
|
||||
))
|
||||
.with(
|
||||
false,
|
||||
() =>
|
||||
attributes && (
|
||||
<div className="flex items-center gap-2">
|
||||
<PropertyWrapper title="Unhandled" visible={!attributes.handled}>
|
||||
<IconBug className="text-sm text-secondary" />
|
||||
</PropertyWrapper>
|
||||
<PropertyWrapper title={attributes.browser} visible={!!attributes.browser}>
|
||||
<PropertyIcon
|
||||
property="$browser"
|
||||
value={attributes.browser}
|
||||
className="text-sm text-secondary"
|
||||
/>
|
||||
</PropertyWrapper>
|
||||
<PropertyWrapper title={attributes.os} visible={!!attributes.os}>
|
||||
<PropertyIcon
|
||||
property="$os"
|
||||
value={attributes.os}
|
||||
className="text-sm text-secondary"
|
||||
/>
|
||||
</PropertyWrapper>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
.exhaustive()}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function PropertyWrapper({
|
||||
title,
|
||||
visible = true,
|
||||
children,
|
||||
}: {
|
||||
title?: string
|
||||
visible?: boolean
|
||||
children: JSX.Element
|
||||
}): JSX.Element {
|
||||
if (Children.count(children) == 0 || title === undefined || !visible) {
|
||||
return <></>
|
||||
}
|
||||
return (
|
||||
<LemonTag>
|
||||
{children}
|
||||
<span className="capitalize">{title}</span>
|
||||
</LemonTag>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ExceptionAttributesPreview'
|
||||
@@ -0,0 +1,163 @@
|
||||
import { IconBox, IconDocument, IconList } from '@posthog/icons'
|
||||
import { LemonCard } from '@posthog/lemon-ui'
|
||||
import { BindLogic, useActions, useValues } from 'kea'
|
||||
import { errorPropertiesLogic, ErrorPropertiesLogicProps } from 'lib/components/Errors/errorPropertiesLogic'
|
||||
import { ErrorEventProperties } from 'lib/components/Errors/types'
|
||||
import { TZLabel } from 'lib/components/TZLabel'
|
||||
import ViewRecordingButton, { mightHaveRecording } from 'lib/components/ViewRecordingButton/ViewRecordingButton'
|
||||
import { IconSubtitles, IconSubtitlesOff } from 'lib/lemon-ui/icons'
|
||||
import { ButtonGroupPrimitive } from 'lib/ui/Button/ButtonPrimitives'
|
||||
import { cn } from 'lib/utils/css-classes'
|
||||
|
||||
import { ErrorTrackingRelationalIssue } from '~/queries/schema/schema-general'
|
||||
|
||||
import { Collapsible } from '../Collapsible'
|
||||
import { ContextDisplay } from '../ContextDisplay'
|
||||
import { ExceptionAttributesPreview } from '../ExceptionAttributesPreview'
|
||||
import { ToggleButtonPrimitive } from '../ToggleButton/ToggleButton'
|
||||
import { exceptionCardLogic } from './exceptionCardLogic'
|
||||
import { StacktraceBaseDisplayProps, StacktraceEmptyDisplay } from './Stacktrace/StacktraceBase'
|
||||
import { StacktraceGenericDisplay } from './Stacktrace/StacktraceGenericDisplay'
|
||||
import { StacktraceTextDisplay } from './Stacktrace/StacktraceTextDisplay'
|
||||
|
||||
interface ExceptionCardContentProps {
|
||||
issue?: ErrorTrackingRelationalIssue
|
||||
issueLoading: boolean
|
||||
}
|
||||
|
||||
export interface ExceptionCardProps extends ExceptionCardContentProps {
|
||||
properties?: ErrorEventProperties
|
||||
propertiesLoading: boolean
|
||||
}
|
||||
|
||||
export function ExceptionCard({ issue, issueLoading, properties, propertiesLoading }: ExceptionCardProps): JSX.Element {
|
||||
return (
|
||||
<BindLogic logic={exceptionCardLogic} props={{ loading: propertiesLoading }}>
|
||||
<BindLogic
|
||||
logic={errorPropertiesLogic}
|
||||
props={{ properties, id: issue?.id ?? 'error' } as ErrorPropertiesLogicProps}
|
||||
>
|
||||
<ExceptionCardContent issue={issue} issueLoading={issueLoading} />
|
||||
</BindLogic>
|
||||
</BindLogic>
|
||||
)
|
||||
}
|
||||
|
||||
function ExceptionCardContent({ issue, issueLoading }: ExceptionCardContentProps): JSX.Element {
|
||||
const { loading, showContext, isExpanded } = useValues(exceptionCardLogic)
|
||||
const { properties, exceptionAttributes, additionalProperties, timestamp, sessionId } =
|
||||
useValues(errorPropertiesLogic)
|
||||
return (
|
||||
<LemonCard hoverEffect={false} className="group py-2 px-3 relative overflow-hidden">
|
||||
<Collapsible isExpanded={isExpanded} className="pb-1 flex w-full" minHeight="calc(var(--spacing) * 12)">
|
||||
<StacktraceIssueDisplay
|
||||
className={cn('flex-grow', showContext && isExpanded ? 'w-2/3' : 'w-full')}
|
||||
truncateMessage={!isExpanded}
|
||||
issue={issue ?? undefined}
|
||||
issueLoading={issueLoading}
|
||||
/>
|
||||
<ContextDisplay
|
||||
className={cn(showContext && isExpanded ? 'w-1/3 pl-2' : 'w-0')}
|
||||
attributes={exceptionAttributes ?? undefined}
|
||||
additionalProperties={additionalProperties}
|
||||
loading={loading}
|
||||
/>
|
||||
</Collapsible>
|
||||
<ExceptionCardActions className="absolute top-2 right-3 flex gap-2 items-center z-10">
|
||||
<ExceptionCardToggles />
|
||||
</ExceptionCardActions>
|
||||
<div className="flex justify-between items-center pt-1">
|
||||
<ExceptionAttributesPreview attributes={exceptionAttributes} loading={loading} />
|
||||
<ExceptionCardActions>
|
||||
{timestamp && <TZLabel className="text-muted text-xs" time={timestamp} />}
|
||||
<ViewRecordingButton
|
||||
sessionId={sessionId}
|
||||
timestamp={timestamp ?? undefined}
|
||||
loading={loading}
|
||||
inModal={true}
|
||||
size="xsmall"
|
||||
type="secondary"
|
||||
disabledReason={mightHaveRecording(properties || {}) ? undefined : 'No recording available'}
|
||||
/>
|
||||
</ExceptionCardActions>
|
||||
</div>
|
||||
</LemonCard>
|
||||
)
|
||||
}
|
||||
|
||||
function ExceptionCardToggles(): JSX.Element {
|
||||
const { showDetails, showAllFrames, showContext, showAsText } = useValues(exceptionCardLogic)
|
||||
const { setShowDetails, setShowAllFrames, setShowContext, setShowAsText } = useActions(exceptionCardLogic)
|
||||
return (
|
||||
<ButtonGroupPrimitive size="sm">
|
||||
<ToggleButtonPrimitive className="px-2" checked={showDetails} onCheckedChange={setShowDetails}>
|
||||
{showDetails ? (
|
||||
<>
|
||||
<IconSubtitlesOff />
|
||||
Hide details
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconSubtitles />
|
||||
Show details
|
||||
</>
|
||||
)}
|
||||
</ToggleButtonPrimitive>
|
||||
<ToggleButtonPrimitive iconOnly checked={showAsText} onCheckedChange={setShowAsText} tooltip="Show as text">
|
||||
<IconDocument />
|
||||
</ToggleButtonPrimitive>
|
||||
<ToggleButtonPrimitive
|
||||
iconOnly
|
||||
checked={showAllFrames}
|
||||
onCheckedChange={setShowAllFrames}
|
||||
tooltip="Show vendor frames"
|
||||
>
|
||||
<IconBox />
|
||||
</ToggleButtonPrimitive>
|
||||
<ToggleButtonPrimitive
|
||||
iconOnly
|
||||
checked={showContext}
|
||||
onCheckedChange={setShowContext}
|
||||
tooltip="Show context"
|
||||
>
|
||||
<IconList />
|
||||
</ToggleButtonPrimitive>
|
||||
</ButtonGroupPrimitive>
|
||||
)
|
||||
}
|
||||
|
||||
function ExceptionCardActions({ children, className }: { children: React.ReactNode; className?: string }): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className={cn('flex justify-between items-center gap-1 bg-surface-primary', className)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StacktraceIssueDisplay({
|
||||
issue,
|
||||
issueLoading,
|
||||
...stacktraceDisplayProps
|
||||
}: {
|
||||
issue?: ErrorTrackingRelationalIssue
|
||||
issueLoading: boolean
|
||||
} & Omit<StacktraceBaseDisplayProps, 'renderLoading' | 'renderEmpty'>): JSX.Element {
|
||||
const { showAsText } = useValues(exceptionCardLogic)
|
||||
const Component = showAsText ? StacktraceTextDisplay : StacktraceGenericDisplay
|
||||
return (
|
||||
<Component
|
||||
{...stacktraceDisplayProps}
|
||||
renderLoading={(renderHeader) =>
|
||||
renderHeader({
|
||||
type: issue?.name ?? undefined,
|
||||
value: issue?.description ?? undefined,
|
||||
loading: issueLoading,
|
||||
})
|
||||
}
|
||||
renderEmpty={() => <StacktraceEmptyDisplay />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +1,13 @@
|
||||
import { EmptyMessage } from 'lib/components/EmptyMessage/EmptyMessage'
|
||||
import { FingerprintRecordPart } from 'lib/components/Errors/stackFrameLogic'
|
||||
import { ExceptionHeaderProps } from 'lib/components/Errors/StackTraces'
|
||||
import { ErrorTrackingRuntime } from 'lib/components/Errors/types'
|
||||
import { ExceptionAttributes } from 'scenes/error-tracking/utils'
|
||||
import { ErrorTrackingRuntime, FingerprintRecordPart } from 'lib/components/Errors/types'
|
||||
|
||||
export type HeaderRenderer = (props: ExceptionHeaderProps) => JSX.Element
|
||||
export interface StacktraceBaseDisplayProps {
|
||||
className?: string
|
||||
truncateMessage: boolean
|
||||
|
||||
loading: boolean
|
||||
renderLoading: (renderHeader: HeaderRenderer) => JSX.Element
|
||||
renderEmpty: () => JSX.Element
|
||||
|
||||
attributes: ExceptionAttributes | null
|
||||
|
||||
showAllFrames: boolean
|
||||
}
|
||||
|
||||
export interface StacktraceBaseExceptionHeaderProps {
|
||||
@@ -1,7 +1,6 @@
|
||||
import { LemonCard } from '@posthog/lemon-ui'
|
||||
import { Meta } from '@storybook/react'
|
||||
import { TEST_EVENTS, TestEventNames } from 'scenes/error-tracking/__mocks__/events'
|
||||
import { getExceptionAttributes } from 'scenes/error-tracking/utils'
|
||||
import { ExceptionLogicWrapper, TEST_EVENTS, TestEventName } from 'scenes/error-tracking/__mocks__/events'
|
||||
import { sceneLogic } from 'scenes/sceneLogic'
|
||||
|
||||
import { mswDecorator } from '~/mocks/browser'
|
||||
@@ -28,7 +27,7 @@ const meta: Meta = {
|
||||
},
|
||||
mswDecorator({
|
||||
post: {
|
||||
'api/environments/:team_id/error_tracking/stack_frames/batch_get/': require('../../__mocks__/stack_frames/batch_get'),
|
||||
'api/environments/:team_id/error_tracking/stack_frames/batch_get/': require('../../../__mocks__/stack_frames/batch_get'),
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -39,57 +38,59 @@ export default meta
|
||||
////////////////////// Generic stacktraces
|
||||
|
||||
export function GenericDisplayPropertiesLoading(): JSX.Element {
|
||||
const props = defaultBaseProps(
|
||||
'python_resolved',
|
||||
{
|
||||
loading: true,
|
||||
},
|
||||
false
|
||||
const props = defaultBaseProps({}, false)
|
||||
return (
|
||||
<ExceptionLogicWrapper eventName="python_resolved" loading={true}>
|
||||
<StacktraceGenericDisplay {...props} />
|
||||
</ExceptionLogicWrapper>
|
||||
)
|
||||
return <StacktraceGenericDisplay {...props} />
|
||||
}
|
||||
|
||||
export function GenericDisplayEmpty(): JSX.Element {
|
||||
const props = defaultBaseProps(
|
||||
'javascript_empty',
|
||||
{
|
||||
loading: false,
|
||||
},
|
||||
false
|
||||
const props = defaultBaseProps({}, false)
|
||||
return (
|
||||
<ExceptionLogicWrapper eventName="javascript_empty">
|
||||
<StacktraceGenericDisplay {...props} />
|
||||
</ExceptionLogicWrapper>
|
||||
)
|
||||
return <StacktraceGenericDisplay {...props} />
|
||||
}
|
||||
|
||||
export function GenericDisplayWithStacktrace(): JSX.Element {
|
||||
return <StacktraceWrapperAllEvents>{(props) => <StacktraceGenericDisplay {...props} />}</StacktraceWrapperAllEvents>
|
||||
const props = defaultBaseProps({}, false)
|
||||
return (
|
||||
<StacktraceWrapperAllEvents>
|
||||
<StacktraceGenericDisplay {...props} />
|
||||
</StacktraceWrapperAllEvents>
|
||||
)
|
||||
}
|
||||
|
||||
///////////////////// Text stacktraces
|
||||
|
||||
export function TextDisplayEmpty(): JSX.Element {
|
||||
const props = defaultBaseProps(
|
||||
'javascript_empty',
|
||||
{
|
||||
loading: false,
|
||||
},
|
||||
false
|
||||
const props = defaultBaseProps({}, false)
|
||||
return (
|
||||
<ExceptionLogicWrapper eventName="javascript_empty">
|
||||
<StacktraceTextDisplay {...props} />
|
||||
</ExceptionLogicWrapper>
|
||||
)
|
||||
return <StacktraceTextDisplay {...props} />
|
||||
}
|
||||
|
||||
export function TextDisplayPropertiesLoading(): JSX.Element {
|
||||
const props = defaultBaseProps(
|
||||
'javascript_resolved',
|
||||
{
|
||||
loading: true,
|
||||
},
|
||||
false
|
||||
const props = defaultBaseProps({}, false)
|
||||
return (
|
||||
<ExceptionLogicWrapper eventName="javascript_resolved">
|
||||
<StacktraceTextDisplay {...props} />
|
||||
</ExceptionLogicWrapper>
|
||||
)
|
||||
return <StacktraceTextDisplay {...props} />
|
||||
}
|
||||
|
||||
export function TextDisplayWithStacktrace(): JSX.Element {
|
||||
return <StacktraceWrapperAllEvents>{(props) => <StacktraceTextDisplay {...props} />}</StacktraceWrapperAllEvents>
|
||||
const props = defaultBaseProps({}, false)
|
||||
return (
|
||||
<StacktraceWrapperAllEvents>
|
||||
<StacktraceTextDisplay {...props} />
|
||||
</StacktraceWrapperAllEvents>
|
||||
)
|
||||
}
|
||||
|
||||
//////////////////// Utils
|
||||
@@ -104,16 +105,11 @@ const issue = {
|
||||
} as ErrorTrackingRelationalIssue
|
||||
|
||||
function defaultBaseProps(
|
||||
event_name: TestEventNames | null,
|
||||
overrideProps: Partial<StacktraceBaseDisplayProps> = {},
|
||||
issueLoading: boolean = false
|
||||
): StacktraceBaseDisplayProps {
|
||||
const attributes = event_name ? getExceptionAttributes(TEST_EVENTS[event_name].properties) : null
|
||||
return {
|
||||
loading: false,
|
||||
showAllFrames: true,
|
||||
truncateMessage: true,
|
||||
attributes,
|
||||
renderLoading: (renderHeader: HeaderRenderer) =>
|
||||
renderHeader({
|
||||
type: issue?.name ?? undefined,
|
||||
@@ -125,29 +121,17 @@ function defaultBaseProps(
|
||||
} as StacktraceBaseDisplayProps
|
||||
}
|
||||
|
||||
function StacktraceWrapperAllEvents({
|
||||
children,
|
||||
}: {
|
||||
children: (props: StacktraceBaseDisplayProps) => JSX.Element
|
||||
}): JSX.Element {
|
||||
const eventNames = Object.keys(TEST_EVENTS) as TestEventNames[]
|
||||
function getProps(eventName: TestEventNames): StacktraceBaseDisplayProps {
|
||||
return defaultBaseProps(
|
||||
eventName,
|
||||
{
|
||||
loading: false,
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
function StacktraceWrapperAllEvents({ children }: { children: JSX.Element }): JSX.Element {
|
||||
const eventNames = Object.keys(TEST_EVENTS) as TestEventName[]
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{eventNames.map((name: TestEventNames) => {
|
||||
const props = getProps(name)
|
||||
{eventNames.map((name: TestEventName) => {
|
||||
return (
|
||||
<LemonCard className="px-3 py-2" key={name}>
|
||||
{children(props)}
|
||||
</LemonCard>
|
||||
<ExceptionLogicWrapper key={name} eventName={name}>
|
||||
<LemonCard hoverEffect={false} className="px-3 py-2">
|
||||
{children}
|
||||
</LemonCard>
|
||||
</ExceptionLogicWrapper>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
@@ -1,24 +1,25 @@
|
||||
import { LemonSkeleton } from '@posthog/lemon-ui'
|
||||
import { useValues } from 'kea'
|
||||
import { errorPropertiesLogic } from 'lib/components/Errors/errorPropertiesLogic'
|
||||
import { FingerprintRecordPartDisplay } from 'lib/components/Errors/FingerprintRecordPartDisplay'
|
||||
import { ChainedStackTraces, ExceptionHeaderProps } from 'lib/components/Errors/StackTraces'
|
||||
import { hasStacktrace } from 'lib/components/Errors/utils'
|
||||
import { cn } from 'lib/utils/css-classes'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { cancelEvent } from '../../utils'
|
||||
import { RuntimeIcon } from '../RuntimeIcon'
|
||||
import { cancelEvent } from '../../../utils'
|
||||
import { RuntimeIcon } from '../../RuntimeIcon'
|
||||
import { exceptionCardLogic } from '../exceptionCardLogic'
|
||||
import { StacktraceBaseDisplayProps, StacktraceBaseExceptionHeaderProps } from './StacktraceBase'
|
||||
|
||||
export function StacktraceGenericDisplay({
|
||||
className,
|
||||
truncateMessage,
|
||||
loading,
|
||||
renderLoading,
|
||||
renderEmpty,
|
||||
showAllFrames,
|
||||
attributes,
|
||||
}: StacktraceBaseDisplayProps): JSX.Element {
|
||||
const { runtime, exceptionList, fingerprintRecords } = attributes || {}
|
||||
const { exceptionAttributes, hasStacktrace } = useValues(errorPropertiesLogic)
|
||||
const { loading, showAllFrames } = useValues(exceptionCardLogic)
|
||||
const { runtime } = exceptionAttributes || {}
|
||||
const renderExceptionHeader = useCallback(
|
||||
({ type, value, loading, part }: ExceptionHeaderProps): JSX.Element => {
|
||||
return (
|
||||
@@ -34,21 +35,18 @@ export function StacktraceGenericDisplay({
|
||||
},
|
||||
[runtime, truncateMessage]
|
||||
)
|
||||
const isEmpty = !hasStacktrace(exceptionList || [])
|
||||
return (
|
||||
<div className={className}>
|
||||
{loading
|
||||
? renderLoading(renderExceptionHeader)
|
||||
: exceptionList && (
|
||||
<ChainedStackTraces
|
||||
showAllFrames={showAllFrames}
|
||||
exceptionList={exceptionList}
|
||||
renderExceptionHeader={renderExceptionHeader}
|
||||
fingerprintRecords={fingerprintRecords}
|
||||
onFrameContextClick={(_, e) => cancelEvent(e)}
|
||||
/>
|
||||
)}
|
||||
{!loading && isEmpty && renderEmpty()}
|
||||
{loading ? (
|
||||
renderLoading(renderExceptionHeader)
|
||||
) : (
|
||||
<ChainedStackTraces
|
||||
showAllFrames={showAllFrames}
|
||||
renderExceptionHeader={renderExceptionHeader}
|
||||
onFrameContextClick={(_, e) => cancelEvent(e)}
|
||||
/>
|
||||
)}
|
||||
{!loading && !hasStacktrace && renderEmpty()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +1,23 @@
|
||||
import { LemonSkeleton } from '@posthog/lemon-ui'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { errorPropertiesLogic } from 'lib/components/Errors/errorPropertiesLogic'
|
||||
import { stackFrameLogic } from 'lib/components/Errors/stackFrameLogic'
|
||||
import { ExceptionHeaderProps } from 'lib/components/Errors/StackTraces'
|
||||
import { ErrorTrackingException, ErrorTrackingStackFrame } from 'lib/components/Errors/types'
|
||||
import { hasStacktrace } from 'lib/components/Errors/utils'
|
||||
import { cn } from 'lib/utils/css-classes'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
|
||||
import { exceptionCardLogic } from '../exceptionCardLogic'
|
||||
import { StacktraceBaseDisplayProps, StacktraceBaseExceptionHeaderProps } from './StacktraceBase'
|
||||
|
||||
export function StacktraceTextDisplay({
|
||||
className,
|
||||
attributes,
|
||||
renderLoading,
|
||||
renderEmpty,
|
||||
truncateMessage,
|
||||
loading,
|
||||
}: StacktraceBaseDisplayProps): JSX.Element {
|
||||
const { exceptionList } = attributes || {}
|
||||
const isEmpty = !hasStacktrace(exceptionList || [])
|
||||
const { exceptionList, hasStacktrace } = useValues(errorPropertiesLogic)
|
||||
const { loading } = useValues(exceptionCardLogic)
|
||||
const renderExceptionHeader = useCallback(
|
||||
({ type, value, loading, part }: ExceptionHeaderProps): JSX.Element => {
|
||||
return (
|
||||
@@ -37,9 +36,10 @@ export function StacktraceTextDisplay({
|
||||
<div className={className}>
|
||||
{loading
|
||||
? renderLoading(renderExceptionHeader)
|
||||
: exceptionList &&
|
||||
exceptionList.map((exception) => <ExceptionTextDisplay key={exception.id} exception={exception} />)}
|
||||
{!loading && isEmpty && renderEmpty()}
|
||||
: exceptionList.map((exception: ErrorTrackingException) => (
|
||||
<ExceptionTextDisplay key={exception.id} exception={exception} />
|
||||
))}
|
||||
{!loading && !hasStacktrace && renderEmpty()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -65,7 +65,7 @@ export function StacktraceTextExceptionHeader({
|
||||
}
|
||||
|
||||
function ExceptionTextDisplay({ exception }: { exception: ErrorTrackingException }): JSX.Element {
|
||||
const { showAllFrames } = useValues(stackFrameLogic)
|
||||
const { showAllFrames } = useValues(exceptionCardLogic)
|
||||
return (
|
||||
<div>
|
||||
<p className="font-mono mb-0 font-bold line-clamp-1">
|
||||
@@ -0,0 +1,65 @@
|
||||
import { actions, kea, listeners, path, props, reducers, selectors } from 'kea'
|
||||
|
||||
import type { exceptionCardLogicType } from './exceptionCardLogicType'
|
||||
|
||||
export interface ExceptionCardLogicProps {
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export const exceptionCardLogic = kea<exceptionCardLogicType>([
|
||||
path(() => ['scenes', 'error-tracking', 'exceptionCardLogic']),
|
||||
props({} as ExceptionCardLogicProps),
|
||||
|
||||
actions({
|
||||
setShowDetails: (showDetails: boolean) => ({ showDetails }),
|
||||
setShowAsText: (showAsText: boolean) => ({ showAsText }),
|
||||
setShowContext: (showContext: boolean) => ({ showContext }),
|
||||
setShowAllFrames: (showAllFrames: boolean) => ({ showAllFrames }),
|
||||
}),
|
||||
|
||||
reducers({
|
||||
showDetails: [
|
||||
false,
|
||||
{ persist: true },
|
||||
{
|
||||
setShowDetails: (_, { showDetails }: { showDetails: boolean }) => showDetails,
|
||||
},
|
||||
],
|
||||
showAsText: [
|
||||
false,
|
||||
{ persist: true },
|
||||
{
|
||||
setShowAsText: (_, { showAsText }: { showAsText: boolean }) => showAsText,
|
||||
},
|
||||
],
|
||||
showAllFrames: [
|
||||
false,
|
||||
{ persist: true },
|
||||
{
|
||||
setShowAllFrames: (_, { showAllFrames }: { showAllFrames: boolean }) => showAllFrames,
|
||||
},
|
||||
],
|
||||
showContext: [
|
||||
true,
|
||||
{ persist: true },
|
||||
{
|
||||
setShowContext: (_, { showContext }: { showContext: boolean }) => showContext,
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
selectors({
|
||||
loading: [() => [(_, props) => props.loading], (loading: boolean) => loading],
|
||||
isExpanded: [
|
||||
(s) => [s.showDetails, s.loading],
|
||||
(showDetails: boolean, loading: boolean) => showDetails && !loading,
|
||||
],
|
||||
}),
|
||||
|
||||
listeners(({ actions }) => {
|
||||
return {
|
||||
setShowContext: () => actions.setShowDetails(true),
|
||||
setShowAllFrames: () => actions.setShowDetails(true),
|
||||
}
|
||||
}),
|
||||
])
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ExceptionCard'
|
||||
@@ -1,198 +0,0 @@
|
||||
import { IconBox, IconDocument, IconList } from '@posthog/icons'
|
||||
import { LemonCard, Spinner } from '@posthog/lemon-ui'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { TZLabel } from 'lib/components/TZLabel'
|
||||
import ViewRecordingButton, { mightHaveRecording } from 'lib/components/ViewRecordingButton/ViewRecordingButton'
|
||||
import { IconSubtitles, IconSubtitlesOff } from 'lib/lemon-ui/icons'
|
||||
import { ButtonGroupPrimitive } from 'lib/ui/Button/ButtonPrimitives'
|
||||
import { cn } from 'lib/utils/css-classes'
|
||||
import { match } from 'ts-pattern'
|
||||
|
||||
import { ErrorTrackingRelationalIssue } from '~/queries/schema/schema-general'
|
||||
|
||||
import { errorTrackingIssueSceneLogic } from '../errorTrackingIssueSceneLogic'
|
||||
import { Collapsible } from './Collapsible'
|
||||
import { ContextDisplay } from './ContextDisplay'
|
||||
import { ExceptionAttributesIconList } from './ExceptionAttributes/ExceptionAttributesIconList'
|
||||
import { StacktraceBaseDisplayProps, StacktraceEmptyDisplay } from './Stacktrace/StacktraceBase'
|
||||
import { StacktraceGenericDisplay } from './Stacktrace/StacktraceGenericDisplay'
|
||||
import { StacktraceTextDisplay } from './Stacktrace/StacktraceTextDisplay'
|
||||
import { ToggleButtonPrimitive } from './ToggleButton/ToggleButton'
|
||||
|
||||
export function IssueCard(): JSX.Element {
|
||||
const {
|
||||
propertiesLoading,
|
||||
firstSeen,
|
||||
issueLoading,
|
||||
properties,
|
||||
exceptionAttributes,
|
||||
additionalProperties,
|
||||
issue,
|
||||
sessionId,
|
||||
showStacktrace,
|
||||
showAllFrames,
|
||||
showContext,
|
||||
showAsText,
|
||||
} = useValues(errorTrackingIssueSceneLogic)
|
||||
const { setShowStacktrace, setShowAllFrames, setShowContext, setShowAsText } =
|
||||
useActions(errorTrackingIssueSceneLogic)
|
||||
const stacktraceDisplayProps = {
|
||||
className: cn('flex-grow', showContext ? 'w-2/3' : 'w-full'),
|
||||
truncateMessage: !showStacktrace,
|
||||
attributes: exceptionAttributes,
|
||||
showAllFrames,
|
||||
showAsText,
|
||||
issue,
|
||||
issueLoading,
|
||||
loading: propertiesLoading,
|
||||
}
|
||||
const contextDisplayProps = {
|
||||
className: cn(showContext && showStacktrace ? 'w-1/3 pl-2' : 'w-0'),
|
||||
attributes: exceptionAttributes,
|
||||
additionalProperties,
|
||||
showContext,
|
||||
loading: propertiesLoading,
|
||||
}
|
||||
return (
|
||||
<LemonCard hoverEffect={false} className="p-0 group p-2 px-3 relative overflow-hidden">
|
||||
<Collapsible
|
||||
isExpanded={showStacktrace && !propertiesLoading}
|
||||
className="pb-1 flex w-full"
|
||||
minHeight="calc(var(--spacing) * 12)"
|
||||
>
|
||||
<StacktraceIssueDisplay {...stacktraceDisplayProps} />
|
||||
<ContextDisplay {...contextDisplayProps} />
|
||||
</Collapsible>
|
||||
<IssueCardActions className="absolute top-2 right-3 flex gap-2 items-center z-10">
|
||||
<ButtonGroupPrimitive size="sm">
|
||||
<ToggleButtonPrimitive
|
||||
className="px-2"
|
||||
checked={showStacktrace}
|
||||
onCheckedChange={() => setShowStacktrace(!showStacktrace)}
|
||||
>
|
||||
{match(showStacktrace)
|
||||
.with(false, () => (
|
||||
<>
|
||||
<IconSubtitles />
|
||||
Show details
|
||||
</>
|
||||
))
|
||||
.with(true, () => (
|
||||
<>
|
||||
<IconSubtitlesOff />
|
||||
Hide details
|
||||
</>
|
||||
))
|
||||
.exhaustive()}
|
||||
</ToggleButtonPrimitive>
|
||||
<ToggleButtonPrimitive
|
||||
iconOnly
|
||||
checked={showAsText}
|
||||
onCheckedChange={setShowAsText}
|
||||
tooltip="Show as text"
|
||||
>
|
||||
<IconDocument />
|
||||
</ToggleButtonPrimitive>
|
||||
<ToggleButtonPrimitive
|
||||
iconOnly
|
||||
checked={showAllFrames}
|
||||
onCheckedChange={setShowAllFrames}
|
||||
tooltip="Show vendor frames"
|
||||
>
|
||||
<IconBox />
|
||||
</ToggleButtonPrimitive>
|
||||
<ToggleButtonPrimitive
|
||||
iconOnly
|
||||
checked={showContext}
|
||||
onCheckedChange={setShowContext}
|
||||
tooltip="Show context"
|
||||
>
|
||||
<IconList />
|
||||
</ToggleButtonPrimitive>
|
||||
</ButtonGroupPrimitive>
|
||||
</IssueCardActions>
|
||||
<div className="flex justify-between items-center pt-1">
|
||||
<EventPropertiesPreview />
|
||||
<IssueCardActions>
|
||||
{
|
||||
// We should timestamp from event properties here but for now only first seen event is accessible and data is not available
|
||||
firstSeen && <TZLabel className="text-muted text-xs" time={firstSeen} />
|
||||
}
|
||||
<ViewRecordingButton
|
||||
sessionId={sessionId}
|
||||
timestamp={properties.timestamp}
|
||||
loading={propertiesLoading}
|
||||
inModal={true}
|
||||
size="xsmall"
|
||||
type="secondary"
|
||||
disabledReason={mightHaveRecording(properties) ? undefined : 'No recording available'}
|
||||
/>
|
||||
</IssueCardActions>
|
||||
</div>
|
||||
</LemonCard>
|
||||
)
|
||||
}
|
||||
|
||||
function EventPropertiesPreview(): JSX.Element {
|
||||
const { exceptionAttributes, propertiesLoading } = useValues(errorTrackingIssueSceneLogic)
|
||||
return (
|
||||
<span className="flex items-center gap-1 text-muted group-hover:text-brand-red">
|
||||
{match(propertiesLoading)
|
||||
.with(true, () => (
|
||||
<span className="text-muted space-x-2 text-xs">
|
||||
<Spinner />
|
||||
<span>Loading details...</span>
|
||||
</span>
|
||||
))
|
||||
.with(false, () => <ExceptionAttributesIconList attributes={exceptionAttributes!} />)
|
||||
.exhaustive()}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function IssueCardActions({
|
||||
children,
|
||||
onlyOnHover = false,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onlyOnHover?: boolean
|
||||
className?: string
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className={cn('flex justify-between items-center gap-1 bg-surface-primary', className, {
|
||||
'opacity-0 group-hover:opacity-100 transition-opacity': onlyOnHover,
|
||||
})}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StacktraceIssueDisplay({
|
||||
showAsText,
|
||||
issue,
|
||||
issueLoading,
|
||||
...stacktraceDisplayProps
|
||||
}: {
|
||||
showAsText: boolean
|
||||
issue: ErrorTrackingRelationalIssue | null
|
||||
issueLoading: boolean
|
||||
} & Omit<StacktraceBaseDisplayProps, 'renderLoading' | 'renderEmpty'>): JSX.Element {
|
||||
const Component = showAsText ? StacktraceTextDisplay : StacktraceGenericDisplay
|
||||
return (
|
||||
<Component
|
||||
{...stacktraceDisplayProps}
|
||||
renderLoading={(renderHeader) =>
|
||||
renderHeader({
|
||||
type: issue?.name ?? undefined,
|
||||
value: issue?.description ?? undefined,
|
||||
loading: issueLoading,
|
||||
})
|
||||
}
|
||||
renderEmpty={() => <StacktraceEmptyDisplay />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -2,13 +2,10 @@ import { actions, connect, defaults, kea, key, listeners, path, props, reducers,
|
||||
import { loaders } from 'kea-loaders'
|
||||
import { actionToUrl, router } from 'kea-router'
|
||||
import api from 'lib/api'
|
||||
import { stackFrameLogic } from 'lib/components/Errors/stackFrameLogic'
|
||||
import { ErrorTrackingException } from 'lib/components/Errors/types'
|
||||
import { hasStacktrace } from 'lib/components/Errors/utils'
|
||||
import { ErrorEventProperties } from 'lib/components/Errors/types'
|
||||
import { Dayjs, dayjs } from 'lib/dayjs'
|
||||
import { objectsEqual } from 'lib/utils'
|
||||
import { posthog } from 'posthog-js'
|
||||
import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic'
|
||||
import { Params, Scene } from 'scenes/sceneTypes'
|
||||
import { urls } from 'scenes/urls'
|
||||
|
||||
@@ -25,14 +22,7 @@ import { ActivityScope, Breadcrumb } from '~/types'
|
||||
import type { errorTrackingIssueSceneLogicType } from './errorTrackingIssueSceneLogicType'
|
||||
import { errorTrackingLogic } from './errorTrackingLogic'
|
||||
import { errorTrackingIssueEventsQuery, errorTrackingIssueQuery } from './queries'
|
||||
import {
|
||||
defaultSearchParams,
|
||||
ExceptionAttributes,
|
||||
getAdditionalProperties,
|
||||
getExceptionAttributes,
|
||||
getSessionId,
|
||||
resolveDateRange,
|
||||
} from './utils'
|
||||
import { defaultSearchParams, resolveDateRange } from './utils'
|
||||
|
||||
export interface ErrorTrackingIssueSceneLogicProps {
|
||||
id: ErrorTrackingIssue['id']
|
||||
@@ -47,27 +37,8 @@ export const errorTrackingIssueSceneLogic = kea<errorTrackingIssueSceneLogicType
|
||||
key((props) => props.id),
|
||||
|
||||
connect(() => ({
|
||||
values: [
|
||||
errorTrackingLogic,
|
||||
['dateRange', 'filterTestAccounts', 'filterGroup', 'searchQuery', 'showStacktrace', 'showContext'],
|
||||
stackFrameLogic,
|
||||
['frameOrderReversed', 'showAllFrames'],
|
||||
preflightLogic,
|
||||
['isCloudOrDev'],
|
||||
],
|
||||
actions: [
|
||||
errorTrackingLogic,
|
||||
[
|
||||
'setDateRange',
|
||||
'setFilterTestAccounts',
|
||||
'setFilterGroup',
|
||||
'setSearchQuery',
|
||||
'setShowStacktrace',
|
||||
'setShowContext',
|
||||
],
|
||||
stackFrameLogic,
|
||||
['setFrameOrderReversed', 'setShowAllFrames'],
|
||||
],
|
||||
values: [errorTrackingLogic, ['dateRange', 'filterTestAccounts', 'filterGroup', 'searchQuery']],
|
||||
actions: [errorTrackingLogic, ['setDateRange', 'setFilterTestAccounts', 'setFilterGroup', 'setSearchQuery']],
|
||||
})),
|
||||
|
||||
actions({
|
||||
@@ -78,16 +49,14 @@ export const errorTrackingIssueSceneLogic = kea<errorTrackingIssueSceneLogicType
|
||||
updateStatus: (status: ErrorTrackingIssueStatus) => ({ status }),
|
||||
updateAssignee: (assignee: ErrorTrackingIssueAssignee | null) => ({ assignee }),
|
||||
setLastSeen: (lastSeen: Dayjs) => ({ lastSeen }),
|
||||
setShowAsText: (showAsText: boolean) => ({ showAsText }),
|
||||
}),
|
||||
|
||||
defaults({
|
||||
issue: null as ErrorTrackingRelationalIssue | null,
|
||||
properties: {} as Record<string, string>,
|
||||
summary: null as ErrorTrackingIssueSummary | null,
|
||||
properties: null as ErrorEventProperties | null,
|
||||
volumeResolution: 50,
|
||||
lastSeen: null as Dayjs | null,
|
||||
showAsText: false as boolean,
|
||||
}),
|
||||
|
||||
reducers({
|
||||
@@ -101,7 +70,6 @@ export const errorTrackingIssueSceneLogic = kea<errorTrackingIssueSceneLogicType
|
||||
},
|
||||
},
|
||||
summary: {},
|
||||
properties: {},
|
||||
volumeResolution: {
|
||||
setVolumeResolution: (_, { volumeResolution }: { volumeResolution: number }) => volumeResolution,
|
||||
},
|
||||
@@ -113,9 +81,6 @@ export const errorTrackingIssueSceneLogic = kea<errorTrackingIssueSceneLogicType
|
||||
return prevLastSeen
|
||||
},
|
||||
},
|
||||
showAsText: {
|
||||
setShowAsText: (_, { showAsText }: { showAsText: boolean }) => showAsText,
|
||||
},
|
||||
}),
|
||||
|
||||
selectors({
|
||||
@@ -165,33 +130,6 @@ export const errorTrackingIssueSceneLogic = kea<errorTrackingIssueSceneLogicType
|
||||
],
|
||||
|
||||
aggregations: [(s) => [s.summary], (summary: ErrorTrackingIssueSummary | null) => summary?.aggregations],
|
||||
exceptionAttributes: [
|
||||
(s) => [s.properties],
|
||||
(properties: Record<string, string>) => (properties ? getExceptionAttributes(properties) : null),
|
||||
],
|
||||
additionalProperties: [
|
||||
(s) => [s.properties, s.isCloudOrDev],
|
||||
(properties: Record<string, string>, isCloudOrDev: boolean | undefined) =>
|
||||
properties ? getAdditionalProperties(properties, isCloudOrDev) : {},
|
||||
],
|
||||
exceptionList: [
|
||||
(s) => [s.exceptionAttributes, s.frameOrderReversed],
|
||||
(attributes: ExceptionAttributes | null, orderReversed: boolean) => {
|
||||
if (!attributes || !attributes.exceptionList) {
|
||||
return []
|
||||
}
|
||||
return applyFrameOrder(attributes.exceptionList, orderReversed)
|
||||
},
|
||||
],
|
||||
fingerprintRecords: [
|
||||
(s) => [s.exceptionAttributes],
|
||||
(attributes: ExceptionAttributes | null) => attributes?.fingerprintRecords,
|
||||
],
|
||||
hasStacktrace: [(s) => [s.exceptionList], (excList: ErrorTrackingException[]) => hasStacktrace(excList)],
|
||||
sessionId: [
|
||||
(s) => [s.properties],
|
||||
(properties: Record<string, string> | null) => (properties ? getSessionId(properties) : undefined),
|
||||
],
|
||||
}),
|
||||
|
||||
loaders(({ values, actions, props }) => ({
|
||||
@@ -264,8 +202,6 @@ export const errorTrackingIssueSceneLogic = kea<errorTrackingIssueSceneLogicType
|
||||
posthog.capture('error_tracking_issue_assigned', { issue_id: props.id })
|
||||
await api.errorTracking.assignIssue(props.id, assignee)
|
||||
},
|
||||
setShowContext: () => actions.setShowStacktrace(true),
|
||||
setShowAllFrames: () => actions.setShowStacktrace(true),
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -309,32 +245,11 @@ export const errorTrackingIssueSceneLogic = kea<errorTrackingIssueSceneLogicType
|
||||
function getPropertiesDateRange(issue: ErrorTrackingRelationalIssue): DateRange {
|
||||
const firstSeen = dayjs(issue.first_seen)
|
||||
return {
|
||||
date_from: firstSeen.startOf('minute').toISOString(),
|
||||
date_to: firstSeen.endOf('minute').toISOString(),
|
||||
date_from: firstSeen.subtract(1, 'hour').toISOString(),
|
||||
date_to: firstSeen.add(1, 'hour').toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
function applyFrameOrder(
|
||||
exceptionList: ErrorTrackingException[],
|
||||
frameOrderReversed: boolean
|
||||
): ErrorTrackingException[] {
|
||||
if (frameOrderReversed) {
|
||||
return exceptionList
|
||||
.map((exception) => {
|
||||
const copiedException = { ...exception }
|
||||
if (copiedException.stacktrace) {
|
||||
copiedException.stacktrace = {
|
||||
...copiedException.stacktrace,
|
||||
frames: copiedException.stacktrace.frames.slice().reverse(),
|
||||
}
|
||||
}
|
||||
return copiedException
|
||||
})
|
||||
.reverse()
|
||||
}
|
||||
return [...exceptionList]
|
||||
}
|
||||
|
||||
export type ErrorTrackingIssueSummary = {
|
||||
aggregations: ErrorTrackingIssueAggregations
|
||||
}
|
||||
|
||||
@@ -27,8 +27,6 @@ export const errorTrackingLogic = kea<errorTrackingLogicType>([
|
||||
setSearchQuery: (searchQuery: string) => ({ searchQuery }),
|
||||
setFilterGroup: (filterGroup: UniversalFiltersGroup) => ({ filterGroup }),
|
||||
setFilterTestAccounts: (filterTestAccounts: boolean) => ({ filterTestAccounts }),
|
||||
setShowStacktrace: (showStacktrace: boolean) => ({ showStacktrace }),
|
||||
setShowContext: (showContext: boolean) => ({ showContext }),
|
||||
}),
|
||||
reducers({
|
||||
dateRange: [
|
||||
@@ -64,20 +62,6 @@ export const errorTrackingLogic = kea<errorTrackingLogicType>([
|
||||
setSearchQuery: (_, { searchQuery }) => searchQuery,
|
||||
},
|
||||
],
|
||||
showStacktrace: [
|
||||
true,
|
||||
{ persist: true },
|
||||
{
|
||||
setShowStacktrace: (_, { showStacktrace }: { showStacktrace: boolean }) => showStacktrace,
|
||||
},
|
||||
],
|
||||
showContext: [
|
||||
true,
|
||||
{ persist: true },
|
||||
{
|
||||
setShowContext: (_, { showContext }: { showContext: boolean }) => showContext,
|
||||
},
|
||||
],
|
||||
}),
|
||||
loaders({
|
||||
hasSentExceptionEvent: {
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { FingerprintRecordPart } from 'lib/components/Errors/stackFrameLogic'
|
||||
import { ErrorTrackingException, ErrorTrackingRuntime } from 'lib/components/Errors/types'
|
||||
import { getRuntimeFromLib } from 'lib/components/Errors/utils'
|
||||
import { ErrorTrackingException } from 'lib/components/Errors/types'
|
||||
import { Dayjs, dayjs } from 'lib/dayjs'
|
||||
import { componentsToDayJs, dateStringToComponents, isStringDateRegex, objectsEqual } from 'lib/utils'
|
||||
import { Properties } from 'posthog-js'
|
||||
import { MouseEvent } from 'react'
|
||||
import { Params } from 'scenes/sceneTypes'
|
||||
|
||||
import { DateRange, ErrorTrackingIssue } from '~/queries/schema/schema-general'
|
||||
import { isPostHogProperty } from '~/taxonomy/taxonomy'
|
||||
|
||||
import { DEFAULT_ERROR_TRACKING_DATE_RANGE, DEFAULT_ERROR_TRACKING_FILTER_GROUP } from './errorTrackingLogic'
|
||||
|
||||
@@ -72,101 +68,6 @@ export const mergeIssues = (
|
||||
}
|
||||
}
|
||||
|
||||
export type ExceptionAttributes = {
|
||||
ingestionErrors?: string[]
|
||||
exceptionList: ErrorTrackingException[]
|
||||
fingerprintRecords: FingerprintRecordPart[]
|
||||
runtime: ErrorTrackingRuntime
|
||||
type?: string
|
||||
value?: string
|
||||
synthetic?: boolean
|
||||
lib?: string
|
||||
libVersion?: string
|
||||
browser?: string
|
||||
browserVersion?: string
|
||||
os?: string
|
||||
osVersion?: string
|
||||
sentryUrl?: string
|
||||
level?: string
|
||||
url?: string
|
||||
handled: boolean
|
||||
}
|
||||
|
||||
export function getExceptionAttributes(properties: Record<string, any>): ExceptionAttributes {
|
||||
const {
|
||||
$lib: lib,
|
||||
$lib_version: libVersion,
|
||||
$browser: browser,
|
||||
$browser_version: browserVersion,
|
||||
$os: os,
|
||||
$os_version: osVersion,
|
||||
$sentry_url: sentryUrl,
|
||||
$sentry_exception,
|
||||
$level: level,
|
||||
$cymbal_errors: ingestionErrors,
|
||||
} = properties
|
||||
|
||||
let type = properties.$exception_type
|
||||
let value = properties.$exception_message
|
||||
let synthetic: boolean | undefined = properties.$exception_synthetic
|
||||
const url: string | undefined = properties.$current_url
|
||||
let exceptionList: ErrorTrackingException[] | undefined = properties.$exception_list
|
||||
const fingerprintRecords: FingerprintRecordPart[] | undefined = properties.$exception_fingerprint_record
|
||||
|
||||
// exception autocapture sets $exception_list for all exceptions.
|
||||
// If it's not present, then this is probably a sentry exception. Get this list from the sentry_exception
|
||||
if (!exceptionList?.length && $sentry_exception) {
|
||||
if (Array.isArray($sentry_exception.values)) {
|
||||
exceptionList = $sentry_exception.values
|
||||
}
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
type = exceptionList?.[0]?.type
|
||||
}
|
||||
if (!value) {
|
||||
value = exceptionList?.[0]?.value
|
||||
}
|
||||
if (synthetic == undefined) {
|
||||
synthetic = exceptionList?.[0]?.mechanism?.synthetic
|
||||
}
|
||||
|
||||
const handled = exceptionList?.[0]?.mechanism?.handled ?? false
|
||||
const runtime: ErrorTrackingRuntime = getRuntimeFromLib(lib)
|
||||
|
||||
return {
|
||||
type,
|
||||
value,
|
||||
synthetic,
|
||||
runtime,
|
||||
lib,
|
||||
libVersion,
|
||||
browser,
|
||||
browserVersion,
|
||||
os,
|
||||
osVersion,
|
||||
url,
|
||||
sentryUrl,
|
||||
exceptionList: exceptionList || [],
|
||||
fingerprintRecords: fingerprintRecords || [],
|
||||
handled,
|
||||
level,
|
||||
ingestionErrors,
|
||||
}
|
||||
}
|
||||
|
||||
export function getAdditionalProperties(properties: Properties, isCloudOrDev: boolean | undefined): Properties {
|
||||
return Object.fromEntries(
|
||||
Object.entries(properties).filter(([key]) => {
|
||||
return !isPostHogProperty(key, isCloudOrDev)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export function getSessionId(properties: Record<string, any>): string | undefined {
|
||||
return properties['$session_id']
|
||||
}
|
||||
|
||||
export function isThirdPartyScriptError(value: ErrorTrackingException['value']): boolean {
|
||||
return value === THIRD_PARTY_SCRIPT_ERROR
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ export function ItemEventDetail({ item }: ItemEventProps): JSX.Element {
|
||||
|
||||
{item.data.fullyLoaded ? (
|
||||
item.data.event === '$exception' ? (
|
||||
<ErrorDisplay eventProperties={item.data.properties} />
|
||||
<ErrorDisplay eventProperties={item.data.properties} eventId={item.data.id} />
|
||||
) : (
|
||||
<LemonTabs
|
||||
size="small"
|
||||
|
||||