import {createRef, Fragment, useLayoutEffect, useMemo, useState} from 'react'; import styled from '@emotion/styled'; import omit from 'lodash/omit'; 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 {REPLAY_CLIP_OFFSETS} from 'sentry/components/events/eventReplay'; import ReplayClipPreview from 'sentry/components/events/eventReplay/replayClipPreview'; import {EventSdk} from 'sentry/components/events/eventSdk'; import NewTagsUI from 'sentry/components/events/eventTagsAndScreenshot/tags'; import {EventViewHierarchy} from 'sentry/components/events/eventViewHierarchy'; import {Breadcrumbs} from 'sentry/components/events/interfaces/breadcrumbs'; import {getFormattedTimeRangeWithLeadingAndTrailingZero} from 'sentry/components/events/interfaces/spans/utils'; import {generateStats} from 'sentry/components/events/opsBreakdown'; import {EventRRWebIntegration} from 'sentry/components/events/rrwebIntegration'; import FileSize from 'sentry/components/fileSize'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import {LazyRender, type LazyRenderProps} from 'sentry/components/lazyRender'; import Link from 'sentry/components/links/link'; import LoadingError from 'sentry/components/loadingError'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import PerformanceDuration from 'sentry/components/performanceDuration'; import QuestionTooltip from 'sentry/components/questionTooltip'; import {Tooltip} from 'sentry/components/tooltip'; import {PAGE_URL_PARAM} from 'sentry/constants/pageFilters'; import {IconChevron, IconOpen} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import { type EntryBreadcrumbs, EntryType, type EventTransaction, type Organization, } from 'sentry/types'; import {objectIsEmpty} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import {getAnalyticsDataForEvent} from 'sentry/utils/events'; import getDynamicText from 'sentry/utils/getDynamicText'; import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants'; import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes'; import {useApiQuery} from 'sentry/utils/queryClient'; import {getReplayIdFromEvent} from 'sentry/utils/replays/getReplayIdFromEvent'; import {useLocation} from 'sentry/utils/useLocation'; import useProjects from 'sentry/utils/useProjects'; import {isCustomMeasurement} from 'sentry/views/dashboards/utils'; import {CustomMetricsEventData} from 'sentry/views/metrics/customMetricsEventData'; import type {TraceTreeNodeDetailsProps} from 'sentry/views/performance/newTraceDetails/traceDrawer/tabs/traceTreeNodeDetails'; import {getTraceTabTitle} from 'sentry/views/performance/newTraceDetails/traceTabs'; import type { TraceTree, TraceTreeNode, } from 'sentry/views/performance/newTraceDetails/traceTree'; import {Row, Tags} from 'sentry/views/performance/traceDetails/styles'; import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils'; import {useTraceAverageTransactionDuration} from '../../useTraceAverageTransactionDuration'; import {IssueList} from './issues/issues'; import {TraceDrawerComponents} from './styles'; 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(); useLayoutEffect(() => { 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 ReplaySection({ event, organization, }: { event: EventTransaction; organization: Organization; }) { const replayId = getReplayIdFromEvent(event); const startTimestampMS = 'startTimestamp' in event ? event.startTimestamp * 1000 : undefined; const timeOfEvent = event.dateCreated ?? startTimestampMS ?? event.dateReceived; const eventTimestampMs = timeOfEvent ? Math.floor(new Date(timeOfEvent).getTime()) : 0; return replayId ? ( {t('Session Replay')} ) : null; } const LAZY_RENDER_PROPS: Partial = { observerOptions: {rootMargin: '50px'}, }; export function TransactionNodeDetails({ node, organization, scrollToNode, onParentClick, }: TraceTreeNodeDetailsProps>) { const location = useLocation(); const {projects} = useProjects(); const issues = useMemo(() => { return [...node.errors, ...node.performance_issues]; }, [node.errors, node.performance_issues]); const {data: averageDurationQueryResult} = useTraceAverageTransactionDuration({ node, location, organization, }); const avgDurationInSeconds: number = useMemo(() => { return ( Number(averageDurationQueryResult?.data[0]?.['avg(transaction.duration)']) / 1000 ); }, [averageDurationQueryResult]); const { data: event, isError, isLoading, } = useApiQuery( [ `/organizations/${organization.slug}/events/${node.value.project_slug}:${node.value.event_id}/`, { query: { referrer: 'trace-details-summary', }, }, ], { staleTime: 0, enabled: !!node, } ); if (isLoading) { return ; } if (isError) { return ; } const project = projects.find(proj => proj.slug === event?.projectSlug); 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 durationInSeconds = endTimestamp - startTimestamp; const measurementNames = Object.keys(node.value.measurements ?? {}) .filter(name => isCustomMeasurement(`measurements.${name}`)) .filter(isNotMarkMeasurement) .filter(isNotPerformanceScoreMeasurement) .sort(); const measurementKeys = Object.keys(event?.measurements ?? {}) .filter(name => Boolean(WEB_VITAL_DETAILS[`measurements.${name}`])) .sort(); const parentTransaction = node.parent_transaction; return (
{t('transaction')}
{' '} {node.value['transaction.op']}
{parentTransaction ? ( onParentClick(parentTransaction)}> {getTraceTabTitle(parentTransaction)} ) : null} {node.value.event_id} {node.value.transaction} {node.value.profile_id ? ( {t('View Profile')} } > {node.value.profile_id} ) : null} {getDynamicText({ fixed: 'Mar 19, 2021 11:06:27 AM UTC', value: ( {` (${startTimeWithLeadingZero})`} ), })}
{getDynamicText({ fixed: 'Mar 19, 2021 11:06:28 AM UTC', value: ( {` (${endTimeWithLeadingZero})`} ), })}
{!event || !event.measurements || measurementKeys.length <= 0 ? null : ( {measurementKeys.map(measurement => ( ))} )} {measurementNames.length > 0 && ( {t('Measurements')} {measurementNames.map(name => { return ( event && ( ) ); })} )}
{organization.features.includes('event-tags-tree-ui') ? ( ) : ( )} {project ? : null} {event.projectSlug ? ( ) : null} {!objectIsEmpty(event.contexts?.feedback ?? {}) ? ( ) : null} {event.user && !objectIsEmpty(event.user) ? ( ) : null} {event._metrics_summary ? ( ) : null} {event.projectSlug ? ( ) : null} {project ? : null} {event.projectSlug ? ( ) : null}
); } const ReplaySectionContainer = styled('div')` display: flex; flex-direction: column; `; const ReplaySectionTitle = styled('div')` font-size: ${p => p.theme.fontSizeMedium}; font-weight: 600; margin-bottom: ${space(2)}; `; const Measurements = styled('div')` display: flex; flex-wrap: wrap; gap: ${space(1)}; padding-top: 10px; `; const TagsWrapper = styled('div')` h3 { color: ${p => p.theme.textColor}; } `;