import {Fragment, useMemo, useState} from 'react'; import type {RouteComponentProps} from 'react-router'; import styled from '@emotion/styled'; import omit from 'lodash/omit'; import {Alert} from 'sentry/components/alert'; import GuideAnchor from 'sentry/components/assistant/guideAnchor'; import ButtonBar from 'sentry/components/buttonBar'; import DiscoverButton from 'sentry/components/discoverButton'; import EventVitals from 'sentry/components/events/eventVitals'; import type {SpanDetailProps} from 'sentry/components/events/interfaces/spans/newTraceDetailsSpanDetails'; import * as Layout from 'sentry/components/layouts/thirds'; import ExternalLink from 'sentry/components/links/externalLink'; import Link from 'sentry/components/links/link'; import LoadingError from 'sentry/components/loadingError'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import {Tooltip} from 'sentry/components/tooltip'; import {IconPlay} from 'sentry/icons'; import {t, tct, tn} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {EventTransaction, Organization} from 'sentry/types'; import {generateQueryWithTag} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import type EventView from 'sentry/utils/discover/eventView'; import {formatTagKey} from 'sentry/utils/discover/fields'; import type {QueryError} from 'sentry/utils/discover/genericDiscoverQuery'; import {getShortEventId} from 'sentry/utils/events'; import {getDuration} from 'sentry/utils/formatters'; import type { TraceError, TraceFullDetailed, TraceMeta, } from 'sentry/utils/performance/quickTrace/types'; import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants'; import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry'; import {useApiQuery} from 'sentry/utils/queryClient'; import useRouter from 'sentry/utils/useRouter'; import {normalizeUrl} from 'sentry/utils/withDomainRequired'; import Tags from 'sentry/views/discover/tags'; import Breadcrumb from 'sentry/views/performance/breadcrumb'; import {MetaData} from 'sentry/views/performance/transactionDetails/styles'; import {BrowserDisplay} from '../transactionDetails/eventMetas'; import NewTraceView from './newTraceDetailsTraceView'; import TraceNotFound from './traceNotFound'; import TraceViewDetailPanel from './traceViewDetailPanel'; import {getTraceInfo, hasTraceData, isRootTransaction} from './utils'; type Props = Pick, 'params' | 'location'> & { dateSelected: boolean; error: QueryError | null; isLoading: boolean; meta: TraceMeta | null; organization: Organization; traceEventView: EventView; traceSlug: string; traces: TraceFullDetailed[] | null; handleLimitChange?: (newLimit: number) => void; orphanErrors?: TraceError[]; }; export enum TraceType { ONE_ROOT = 'one_root', NO_ROOT = 'no_root', MULTIPLE_ROOTS = 'multiple_roots', BROKEN_SUBTRACES = 'broken_subtraces', ONLY_ERRORS = 'only_errors', EMPTY_TRACE = 'empty_trace', } export type EventDetail = { event: EventTransaction | undefined; openPanel: string | undefined; traceFullDetailedEvent: TraceFullDetailed; }; function NewTraceDetailsContent(props: Props) { const router = useRouter(); const [detail, setDetail] = useState( undefined ); const traceInfo = useMemo( () => getTraceInfo(props.traces ?? [], props.orphanErrors), [props.traces, props.orphanErrors] ); const root = props.traces && props.traces[0]; const {data: rootEvent, isLoading: isRootEventLoading} = useApiQuery( [ `/organizations/${props.organization.slug}/events/${root?.project_slug}:${root?.event_id}/`, { query: { referrer: 'trace-details-summary', }, }, ], { staleTime: Infinity, enabled: !!(props.traces && props.traces.length > 0), } ); const renderTraceLoading = () => { return ( {t('Hang in there, as we build your trace view!')} ); }; const renderTraceRequiresDateRangeSelection = () => { return ; }; const renderTraceHeader = () => { const {meta} = props; const errors = meta?.errors ?? traceInfo.errors.size; const performanceIssues = meta?.performance_issues ?? traceInfo.performanceIssues.size; const replay_id = rootEvent?.contexts.replay?.replay_id ?? ''; return ( {rootEvent && ( } subtext={null} /> {replay_id && ( {getShortEventId(replay_id)} } subtext={null} /> )} )} 0 ? (
{tn('%s error issue', '%s error issues', errors)}
{tn( '%s performance issue', '%s performance issues', performanceIssues )}
) : null } showUnderline position="bottom" > {errors + performanceIssues} } subtext={null} />
); }; const getTraceType = (): TraceType => { const {traces, orphanErrors} = props; const {roots, orphans} = (traces ?? []).reduce( (counts, trace) => { if (isRootTransaction(trace)) { counts.roots++; } else { counts.orphans++; } return counts; }, {roots: 0, orphans: 0} ); if (roots === 0 && orphans > 0) { return TraceType.NO_ROOT; } if (roots === 1 && orphans > 0) { return TraceType.BROKEN_SUBTRACES; } if (roots > 1) { return TraceType.MULTIPLE_ROOTS; } if (orphanErrors && orphanErrors.length > 1) { return TraceType.ONLY_ERRORS; } if (roots === 1) { return TraceType.ONE_ROOT; } if (roots === 0 && orphans === 0) { return TraceType.EMPTY_TRACE; } throw new Error('Unknown trace type'); }; const renderTraceWarnings = () => { let warning: React.ReactNode = null; const traceType = getTraceType(); switch (traceType) { case TraceType.NO_ROOT: warning = ( {t( 'A root transaction is missing. Transactions linked by a dashed line have been orphaned and cannot be directly linked to the root.' )} ); break; case TraceType.BROKEN_SUBTRACES: warning = ( {t( 'This trace has broken subtraces. Transactions linked by a dashed line have been orphaned and cannot be directly linked to the root.' )} ); break; case TraceType.MULTIPLE_ROOTS: warning = ( {t('Multiple root transactions have been found with this trace ID.')} ); break; case TraceType.ONLY_ERRORS: warning = ( {tct( "The good news is we know these errors are related to each other. The bad news is that we can't tell you more than that. If you haven't already, [tracingLink: configure performance monitoring for your SDKs] to learn more about service interactions.", { tracingLink: ( ), } )} ); break; default: } return warning; }; const renderFooter = () => { const {traceEventView, organization, location, meta, orphanErrors} = props; const orphanErrorsCount = orphanErrors?.length ?? 0; const transactionsCount = meta?.transactions ?? traceInfo?.transactions.size ?? 0; const totalNumOfEvents = transactionsCount + orphanErrorsCount; const webVitals = Object.keys(rootEvent?.measurements ?? {}) .filter(name => Boolean(WEB_VITAL_DETAILS[`measurements.${name}`])) .sort(); return ( rootEvent && ( {webVitals.length > 0 && (
)}
{ const url = traceEventView.getResultsViewUrlTarget( organization.slug, false ); url.query = generateQueryWithTag(url.query, { key: formatTagKey(key), value, }); return url; }} totalValues={totalNumOfEvents} eventView={traceEventView} organization={organization} location={location} />
) ); }; const renderContent = () => { const { dateSelected, isLoading, error, organization, location, traceEventView, traceSlug, traces, meta, orphanErrors, } = props; if (!dateSelected) { return renderTraceRequiresDateRangeSelection(); } const hasOrphanErrors = orphanErrors && orphanErrors.length > 0; const onlyOrphanErrors = hasOrphanErrors && (!traces || traces.length === 0); const hasData = hasTraceData(traces, orphanErrors); if (isLoading || (isRootEventLoading && hasData && !onlyOrphanErrors)) { return renderTraceLoading(); } if (error !== null || !hasData) { return ( ); } return ( {renderTraceWarnings()} {traceInfo && renderTraceHeader()} {renderFooter()} { router.replace({ ...location, hash: undefined, query: { ...omit(location.query, 'openPanel'), }, }); setDetail(undefined); }} /> ); }; const {organization, location, traceEventView, traceSlug} = props; return ( {t('Trace ID: %s', traceSlug)} { trackAnalytics('performance_views.trace_view.open_in_discover', { organization, }); }} > {t('Open in Discover')} {renderContent()} ); } const StyledLoadingIndicator = styled(LoadingIndicator)` margin-bottom: 0; `; const LoadingContainer = styled('div')` font-size: ${p => p.theme.fontSizeLarge}; color: ${p => p.theme.subText}; text-align: center; `; const ReplayLinkBody = styled('div')` display: flex; align-items: center; gap: ${space(0.25)}; `; const TraceHeaderContainer = styled('div')` display: flex; align-items: center; justify-content: space-between; `; const TraceHeaderRow = styled('div')` display: flex; align-items: center; gap: ${space(2)}; `; const Margin = styled('div')` margin-top: ${space(2)}; `; const TraceHeaderWrapper = styled('div')` display: flex; gap: ${space(2)}; `; export default NewTraceDetailsContent;