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 {LinkButton} 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 type {SpanDetailProps} from 'sentry/components/events/interfaces/spans/newTraceDetailsSpanDetails'; import NewTraceDetailsSpanDetail, { SpanDetailContainer, SpanDetails, } from 'sentry/components/events/interfaces/spans/newTraceDetailsSpanDetails'; import { getFormattedTimeRangeWithLeadingAndTrailingZero, getSpanOperation, } 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 {CustomMetricsEventData} from 'sentry/components/metrics/customMetricsEventData'; 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} from 'sentry/types/event'; import {EntryType} from 'sentry/types/event'; import type {Organization} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; import getDynamicText from 'sentry/utils/getDynamicText'; import {isEmptyObject} from 'sentry/utils/object/isEmptyObject'; 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 {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 DetailPanel from 'sentry/views/insights/common/components/detailPanel'; import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider'; import {ProfileContext, ProfilesProvider} from 'sentry/views/profiling/profilesProvider'; import {transactionSummaryRouteWithQuery} from '../transactionSummary/utils'; import type {EventDetail} from './newTraceDetailsContent'; import {Row, Tags} from './styles'; type DetailPanelProps = { detail: EventDetail | SpanDetailProps | undefined; onClose: () => void; }; type EventDetailProps = { detail: EventDetail; location: Location; 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({detail, organization, location}: EventDetailProps) { const {projects} = useProjects(); if (!detail.event) { return ; } const {user, contexts, projectSlug} = detail.event; const {feedback} = contexts ?? {}; const eventJsonUrl = `/api/0/projects/${organization.slug}/${detail.traceFullDetailedEvent.project_slug}/events/${detail.traceFullDetailedEvent.event_id}/json/`; const project = projects.find(proj => proj.slug === detail.event?.projectSlug); const {errors, performance_issues} = detail.traceFullDetailedEvent; const hasIssues = errors.length + performance_issues.length > 0; const startTimestamp = Math.min( detail.traceFullDetailedEvent.start_timestamp, detail.traceFullDetailedEvent.timestamp ); const endTimestamp = Math.max( detail.traceFullDetailedEvent.start_timestamp, detail.traceFullDetailedEvent.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(detail.traceFullDetailedEvent.measurements ?? {}) .filter(name => isCustomMeasurement(`measurements.${name}`)) .filter(isNotMarkMeasurement) .filter(isNotPerformanceScoreMeasurement) .sort(); const renderMeasurements = () => { if (!detail.event) { return null; } const {measurements} = detail.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 (!detail.traceFullDetailedEvent.profile_id) { return null; } const target = generateProfileFlamechartRoute({ orgSlug: organization.slug, projectSlug: detail.traceFullDetailedEvent.project_slug, profileId: detail.traceFullDetailedEvent.profile_id, }); function handleOnClick() { trackAnalytics('profiling_views.go_to_flamegraph', { organization, source: 'performance.trace_view', }); } return ( {t('View Profile')} ); }; return ( } href={eventJsonUrl} external onClick={() => trackAnalytics('performance_views.event_details.json_button_click', { organization, }) } > {t('JSON')} () <Tooltip title={detail.traceFullDetailedEvent.project_slug}> <ProjectBadge project={ project ? project : {slug: detail.traceFullDetailedEvent.project_slug} } avatarSize={50} hideName /> </Tooltip> <div> <div>{t('Event')}</div> <TransactionOp> {' '} {detail.traceFullDetailedEvent['transaction.op']} </TransactionOp> </div> {hasIssues && ( ( {error.level} {error.title} ))} > {tn( '%s issue occurred in this transaction.', '%s issues occurred in this transaction.', detail.traceFullDetailedEvent.errors.length + detail.traceFullDetailedEvent.performance_issues.length )} )} {t('Event ID')}}> {detail.traceFullDetailedEvent.event_id} {detail.traceFullDetailedEvent.transaction} {detail.traceFullDetailedEvent.profile_id && ( {detail.traceFullDetailedEvent.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 ( detail.event && ( ) ); })} )}
{project && } {projectSlug && ( )} {!isEmptyObject(feedback) && ( )} {user && !isEmptyObject(user) && ( )} {detail.event._metrics_summary ? ( ) : null} {project && ( )} {project && } {projectSlug && ( )}
); } function SpanDetailsBody({ detail, organization, }: { detail: SpanDetailProps; organization: Organization; }) { const {projects} = useProjects(); const project = projects.find(proj => proj.slug === detail.event?.projectSlug); const profileId = detail?.event?.contexts?.profile?.profile_id ?? null; return ( <Tooltip title={detail.event.projectSlug}> <ProjectBadge project={project ? project : {slug: detail.event.projectSlug || ''}} avatarSize={50} hideName /> </Tooltip> <div> <div>{t('Span')}</div> <TransactionOp> {getSpanOperation(detail.node.value)}</TransactionOp> </div> {detail.event.projectSlug && ( {profiles => ( )} )} ); } export function isEventDetail( detail: EventDetail | SpanDetailProps ): detail is EventDetail { return !('span' in detail); } function TraceViewDetailPanel({detail, onClose}: DetailPanelProps) { const organization = useOrganization(); const location = useLocation(); return ( {detail && (isEventDetail(detail) ? ( ) : ( ))} ); } 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: ${p => p.theme.fontWeightBold}; 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(LinkButton)` position: absolute; top: ${space(0.75)}; right: ${space(0.5)}; `; const StyledTable = styled('table')` margin-bottom: 0 !important; `; export default TraceViewDetailPanel;