import {Fragment, useMemo, useState} from 'react'; import styled from '@emotion/styled'; import startCase from 'lodash/startCase'; import moment from 'moment-timezone'; import {Button} from 'sentry/components/button'; import KeyValueList from 'sentry/components/events/interfaces/keyValueList'; import {AnnotatedText} from 'sentry/components/events/meta/annotatedText'; import ExternalLink from 'sentry/components/links/externalLink'; import ListItem from 'sentry/components/list/listItem'; import {JavascriptProcessingErrors} from 'sentry/constants/eventErrors'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; export type EventErrorData = { message: React.ReactNode; type: string; data?: { image_name?: string; image_path?: string; message?: string; name?: string; sdk_time?: string; server_time?: string; url?: string; } & Record; }; const keyMapping = { image_uuid: 'Debug ID', image_name: 'File Name', image_path: 'File Path', }; export type ErrorItemProps = { error: EventErrorData; meta?: Record; }; export function ErrorItem({error, meta}: ErrorItemProps) { const [expanded, setExpanded] = useState(false); const cleanedData = useMemo(() => { const data = {...(error.data ?? {})}; // The name is rendered as path in front of the message if (typeof data.name === 'string') { delete data.name; } if (data.message === 'None') { // Python ensures a message string, but "None" doesn't make sense here delete data.message; } if (typeof data.image_path === 'string') { // Separate the image name for readability const separator = /^([a-z]:\\|\\\\)/i.test(data.image_path) ? '\\' : '/'; const path = data.image_path.split(separator); data.image_name = path.splice(-1, 1)[0]; data.image_path = path.length ? path.join(separator) + separator : ''; } if (typeof data.server_time === 'string' && typeof data.sdk_time === 'string') { data.message = t( 'Adjusted timestamps by %s', moment .duration(moment.utc(data.server_time).diff(moment.utc(data.sdk_time))) .humanize() ); } return Object.entries(data) .map(([key, value]) => ({ key, value, subject: keyMapping[key] || startCase(key), meta: key === 'image_name' ? meta?.image_path?.[''] : meta?.[key]?.[''], })) .filter(d => { if (!d.value && !!d.meta) { return true; } return !!d.value; }); }, [error.data, meta]); return (
{meta?.data?.name?.[''] ? ( ) : !error.data?.name || typeof error.data?.name !== 'string' ? null : ( {error.data?.name} {': '} )} {meta?.message?.[''] ? ( ) : ( error.message )} {Object.values(JavascriptProcessingErrors).includes( error.type as JavascriptProcessingErrors ) && ( {' '} ( {tct('see [docsLink]', { docsLink: ( {t('Troubleshooting for JavaScript')} ), })} ) )}
{!!cleanedData.length && ( { event.stopPropagation(); setExpanded(!expanded); }} priority="link" size="zero" > {expanded ? t('Collapse') : t('Expand')} )}
{expanded && }
); } const ToggleButton = styled(Button)` margin-left: ${space(1.5)}; font-weight: ${p => p.theme.fontWeightBold}; color: ${p => p.theme.subText}; :hover, :focus { color: ${p => p.theme.textColor}; } `; const StyledListItem = styled(ListItem)` margin-bottom: ${space(0.75)}; `; const StyledExternalLink = styled(ExternalLink)` /* && is here to increase specificity to override default styles*/ && { font-weight: inherit; color: inherit; text-decoration: underline; } `; const OverallInfo = styled('div')` display: grid; grid-template-columns: repeat(2, minmax(auto, max-content)); word-break: break-all; `;