import {createRef, Fragment, useEffect, useState} from 'react'; import styled from '@emotion/styled'; import type {Location} from 'history'; import omit from 'lodash/omit'; import Alert from 'sentry/components/alert'; import {Button} from 'sentry/components/button'; import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton'; import DateTime from 'sentry/components/dateTime'; import {Chunk} from 'sentry/components/events/contexts/chunk'; import {EventAttachments} from 'sentry/components/events/eventAttachments'; import { isNotMarkMeasurement, isNotPerformanceScoreMeasurement, TraceEventCustomPerformanceMetric, } from 'sentry/components/events/eventCustomPerformanceMetrics'; import {Entries} from 'sentry/components/events/eventEntries'; import {EventEvidence} from 'sentry/components/events/eventEvidence'; import {EventExtraData} from 'sentry/components/events/eventExtraData'; import {EventSdk} from 'sentry/components/events/eventSdk'; import {EventViewHierarchy} from 'sentry/components/events/eventViewHierarchy'; import {Breadcrumbs} from 'sentry/components/events/interfaces/breadcrumbs'; import NewTraceDetailsSpanDetail, { SpanDetailContainer, SpanDetails, } from 'sentry/components/events/interfaces/spans/newTraceDetailsSpanDetails'; import { getFormattedTimeRangeWithLeadingAndTrailingZero, getSpanOperation, parseTrace, } from 'sentry/components/events/interfaces/spans/utils'; import {generateStats} from 'sentry/components/events/opsBreakdown'; import {EventRRWebIntegration} from 'sentry/components/events/rrwebIntegration'; import {DataSection} from 'sentry/components/events/styles'; import FileSize from 'sentry/components/fileSize'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import Link from 'sentry/components/links/link'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import { ErrorDot, ErrorLevel, ErrorMessageContent, ErrorMessageTitle, ErrorTitle, } from 'sentry/components/performance/waterfall/rowDetails'; import PerformanceDuration from 'sentry/components/performanceDuration'; import QuestionTooltip from 'sentry/components/questionTooltip'; import {generateIssueEventTarget} from 'sentry/components/quickTrace/utils'; import {Tooltip} from 'sentry/components/tooltip'; import {PAGE_URL_PARAM} from 'sentry/constants/pageFilters'; import {IconChevron, IconOpen} from 'sentry/icons'; import {t, tn} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {EntryBreadcrumbs, EventTransaction, Organization} from 'sentry/types'; import {EntryType} from 'sentry/types'; import {objectIsEmpty} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import getDynamicText from 'sentry/utils/getDynamicText'; import {PageAlertProvider} from 'sentry/utils/performance/contexts/pageAlert'; import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants'; import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes'; import {useApiQuery} from 'sentry/utils/queryClient'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import useProjects from 'sentry/utils/useProjects'; import {isCustomMeasurement} from 'sentry/views/dashboards/utils'; import {CustomMetricsEventData} from 'sentry/views/ddm/customMetricsEventData'; import { isSpanNode, isTransactionNode, } from 'sentry/views/performance/newTraceDetails/guards'; import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider'; import {ProfileContext, ProfilesProvider} from 'sentry/views/profiling/profilesProvider'; import DetailPanel from 'sentry/views/starfish/components/detailPanel'; import {Row, Tags} from '../traceDetails/styles'; import {transactionSummaryRouteWithQuery} from '../transactionSummary/utils'; import type {TraceTree, TraceTreeNode} from './traceTree'; type EventDetailProps = { location: Location; node: TraceTreeNode; organization: Organization; }; function OpsBreakdown({event}: {event: EventTransaction}) { const [showingAll, setShowingAll] = useState(false); const breakdown = event && generateStats(event, {type: 'no_filter'}); if (!breakdown) { return null; } const renderText = showingAll ? t('Show less') : t('Show more') + '...'; return ( breakdown && ( {t('Ops Breakdown')} } >
{breakdown.slice(0, showingAll ? breakdown.length : 5).map(currOp => { const {name, percentage, totalInterval} = currOp; const operationName = typeof name === 'string' ? name : t('Other'); const pctLabel = isFinite(percentage) ? Math.round(percentage * 100) : '∞'; return (
{operationName}:{' '} ({pctLabel}%)
); })} {breakdown.length > 5 && ( setShowingAll(prev => !prev)}>{renderText} )}
) ); } function BreadCrumbsSection({ event, organization, }: { event: EventTransaction; organization: Organization; }) { const [showBreadCrumbs, setShowBreadCrumbs] = useState(false); const breadCrumbsContainerRef = createRef(); useEffect(() => { setTimeout(() => { if (showBreadCrumbs) { breadCrumbsContainerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end', }); } }, 100); }, [showBreadCrumbs, breadCrumbsContainerRef]); const matchingEntry: EntryBreadcrumbs | undefined = event?.entries.find( (entry): entry is EntryBreadcrumbs => entry.type === EntryType.BREADCRUMBS ); if (!matchingEntry) { return null; } const renderText = showBreadCrumbs ? t('Hide Breadcrumbs') : t('Show Breadcrumbs'); const chevron = ; return ( { setShowBreadCrumbs(prev => !prev); }} > {renderText} {chevron}
{showBreadCrumbs && ( )}
); } function EventDetails({node, organization, location}: EventDetailProps) { const {projects} = useProjects(); const {data: event} = useApiQuery( [ `/organizations/${organization.slug}/events/${node.value.project_slug}:${node.value.event_id}/`, { query: { referrer: 'trace-details-summary', }, }, ], { staleTime: 0, enabled: !!node, } ); if (!event) { return ; } const {user, contexts, projectSlug} = event; const {feedback} = contexts ?? {}; const eventJsonUrl = `/api/0/projects/${organization.slug}/${node.value.project_slug}/events/${node.value.event_id}/json/`; const project = projects.find(proj => proj.slug === event?.projectSlug); const {errors, performance_issues} = node.value; const hasIssues = errors.length + performance_issues.length > 0; const startTimestamp = Math.min(node.value.start_timestamp, node.value.timestamp); const endTimestamp = Math.max(node.value.start_timestamp, node.value.timestamp); const {start: startTimeWithLeadingZero, end: endTimeWithLeadingZero} = getFormattedTimeRangeWithLeadingAndTrailingZero(startTimestamp, endTimestamp); const duration = (endTimestamp - startTimestamp) * 1000; const durationString = `${Number(duration.toFixed(3)).toLocaleString()}ms`; const measurementNames = Object.keys(node.value.measurements ?? {}) .filter(name => isCustomMeasurement(`measurements.${name}`)) .filter(isNotMarkMeasurement) .filter(isNotPerformanceScoreMeasurement) .sort(); const renderMeasurements = () => { if (!event) { return null; } const {measurements} = event; const measurementKeys = Object.keys(measurements ?? {}) .filter(name => Boolean(WEB_VITAL_DETAILS[`measurements.${name}`])) .sort(); if (!measurements || measurementKeys.length <= 0) { return null; } return ( {measurementKeys.map(measurement => ( ))} ); }; const renderGoToProfileButton = () => { if (!node.value.profile_id) { return null; } const target = generateProfileFlamechartRoute({ orgSlug: organization.slug, projectSlug: node.value.project_slug, profileId: node.value.profile_id, }); function handleOnClick() { trackAnalytics('profiling_views.go_to_flamegraph', { organization, source: 'performance.trace_view', }); } return ( {t('View Profile')} ); }; return ( <Tooltip title={node.value.project_slug}> <ProjectBadge project={project ? project : {slug: node.value.project_slug}} avatarSize={50} hideName /> </Tooltip> <div> <div>{t('Event')}</div> <TransactionOp> {node.value['transaction.op']}</TransactionOp> </div> {hasIssues && ( ( {error.level} {error.title} ))} > {tn( '%s issue occurred in this transaction.', '%s issues occurred in this transaction.', node.value.errors.length + node.value.performance_issues.length )} )} {t('Event ID')}}> {node.value.event_id} {node.value.transaction} {node.value.profile_id && ( {node.value.profile_id} )} {durationString} {getDynamicText({ fixed: 'Mar 19, 2021 11:06:27 AM UTC', value: ( {` (${startTimeWithLeadingZero})`} ), })}
{getDynamicText({ fixed: 'Mar 19, 2021 11:06:28 AM UTC', value: ( {` (${endTimeWithLeadingZero})`} ), })}
{renderMeasurements()} {measurementNames.length > 0 && ( {t('Measurements')} {measurementNames.map(name => { return ( event && ( ) ); })} )}
{project && } {projectSlug && ( )} {!objectIsEmpty(feedback) && ( )} {user && !objectIsEmpty(user) && ( )} {event._metrics_summary ? ( ) : null} {projectSlug && } {project && } {projectSlug && ( )}
); } function SpanDetailsBody({ node, organization, }: { node: TraceTreeNode; organization: Organization; }) { const {projects} = useProjects(); const {event, relatedErrors, childTxn, ...span} = node.value; const project = projects.find(proj => proj.slug === event?.projectSlug); const profileId = event?.contexts?.profile?.profile_id ?? null; return ( <Tooltip title={event.projectSlug}> <ProjectBadge project={project ? project : {slug: event.projectSlug || ''}} avatarSize={50} hideName /> </Tooltip> <div> <div>{t('Span')}</div> <TransactionOp> {getSpanOperation(span)}</TransactionOp> </div> {event.projectSlug && ( {profiles => ( )} )} ); } interface TraceDetailPanelProps { node: TraceTreeNode | null; onClose: () => void; } function TraceDetailPanel(props: TraceDetailPanelProps) { const location = useLocation(); const organization = useOrganization(); if (props.node && !(isTransactionNode(props.node) || isSpanNode(props.node))) { return null; } return ( {props.node && (isTransactionNode(props.node) ? ( ) : ( ))} ); } const Wrapper = styled('div')` display: flex; flex-direction: column; gap: ${space(2)}; ${DataSection} { padding: 0; } ${SpanDetails} { padding: 0; } ${SpanDetailContainer} { border-bottom: none; } `; const FlexBox = styled('div')` display: flex; align-items: center; `; const Actions = styled('div')` display: flex; align-items: center; justify-content: flex-end; `; const Title = styled(FlexBox)` gap: ${space(2)}; `; const TransactionOp = styled('div')` font-size: 25px; font-weight: bold; max-width: 600px; ${p => p.theme.overflowEllipsis} `; const TransactionIdTitle = styled('a')` display: flex; color: ${p => p.theme.textColor}; :hover { color: ${p => p.theme.textColor}; } `; const Measurements = styled('div')` display: flex; flex-wrap: wrap; gap: ${space(1)}; padding-top: 10px; `; const StyledButton = styled(Button)` position: absolute; top: ${space(0.75)}; right: ${space(0.5)}; `; const StyledTable = styled('table')` margin-bottom: 0 !important; `; export default TraceDetailPanel;