import {Component, createRef, Fragment} from 'react'; import type {RouteComponentProps} from 'react-router'; import styled from '@emotion/styled'; 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 * as Layout from 'sentry/components/layouts/thirds'; import ExternalLink from 'sentry/components/links/externalLink'; import LoadingError from 'sentry/components/loadingError'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import TimeSince from 'sentry/components/timeSince'; import {t, tct, tn} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types'; import {defined} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import type EventView from 'sentry/utils/discover/eventView'; import type {QueryError} from 'sentry/utils/discover/genericDiscoverQuery'; import {getDuration} from 'sentry/utils/formatters'; import type {Fuse} from 'sentry/utils/fuzzySearch'; import {createFuzzySearch} from 'sentry/utils/fuzzySearch'; import getDynamicText from 'sentry/utils/getDynamicText'; import type { TraceError, TraceFullDetailed, TraceMeta, } from 'sentry/utils/performance/quickTrace/types'; import {filterTrace, reduceTrace} from 'sentry/utils/performance/quickTrace/utils'; import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry'; import Breadcrumb from 'sentry/views/performance/breadcrumb'; import {MetaData} from 'sentry/views/performance/transactionDetails/styles'; import {TraceDetailHeader, TraceSearchBar, TraceSearchContainer} from './styles'; import TraceNotFound from './traceNotFound'; import TraceView from './traceView'; import type {TraceInfo} from './types'; import {getTraceInfo, hasTraceData, isRootTransaction} from './utils'; type IndexedFusedTransaction = { event: TraceFullDetailed | TraceError; indexed: string[]; }; 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[]; }; type State = { filteredEventIds: Set | undefined; searchQuery: string | undefined; }; class TraceDetailsContent extends Component { state: State = { searchQuery: undefined, filteredEventIds: undefined, }; componentDidMount() { this.initFuse(); } componentDidUpdate(prevProps: Props) { if ( this.props.traces !== prevProps.traces || this.props.orphanErrors !== prevProps.orphanErrors ) { this.initFuse(); } } fuse: Fuse | null = null; traceViewRef = createRef(); virtualScrollbarContainerRef = createRef(); async initFuse() { const {traces, orphanErrors} = this.props; if (!hasTraceData(traces, orphanErrors)) { return; } const transformedEvents: IndexedFusedTransaction[] = traces?.flatMap(trace => reduceTrace( trace, (acc, transaction) => { const indexed: string[] = [ transaction['transaction.op'], transaction.transaction, transaction.project_slug, ]; acc.push({ event: transaction, indexed, }); return acc; }, [] ) ) ?? []; // Include orphan error titles and project slugs during fuzzy search orphanErrors?.forEach(orphanError => { const indexed: string[] = [orphanError.title, orphanError.project_slug, 'Unknown']; transformedEvents.push({ indexed, event: orphanError, }); }); this.fuse = await createFuzzySearch(transformedEvents, { keys: ['indexed'], includeMatches: true, threshold: 0.6, location: 0, distance: 100, maxPatternLength: 32, }); } renderTraceLoading() { return ( {t('Hang in there, as we build your trace view!')} ); } renderTraceRequiresDateRangeSelection() { return ; } handleTransactionFilter = (searchQuery: string) => { this.setState({searchQuery: searchQuery || undefined}, this.filterTransactions); }; filterTransactions = () => { const {traces, orphanErrors} = this.props; const {filteredEventIds, searchQuery} = this.state; if (!searchQuery || !hasTraceData(traces, orphanErrors) || !defined(this.fuse)) { if (filteredEventIds !== undefined) { this.setState({ filteredEventIds: undefined, }); } return; } const fuseMatches = this.fuse .search(searchQuery) /** * Sometimes, there can be matches that don't include any * indices. These matches are often noise, so exclude them. */ .filter(({matches}) => matches?.length) .map(({item}) => item.event.event_id); /** * Fuzzy search on ids result in seemingly random results. So switch to * doing substring matches on ids to provide more meaningful results. */ const idMatches: string[] = []; traces ?.flatMap(trace => filterTrace( trace, ({event_id, span_id}) => event_id.includes(searchQuery) || span_id.includes(searchQuery) ) ) .forEach(transaction => idMatches.push(transaction.event_id)); // Include orphan error event_ids and span_ids during substring search orphanErrors?.forEach(orphanError => { const {event_id, span} = orphanError; if (event_id.includes(searchQuery) || span.includes(searchQuery)) { idMatches.push(event_id); } }); this.setState({ filteredEventIds: new Set([...fuseMatches, ...idMatches]), }); }; renderSearchBar() { return ( ); } renderTraceHeader(traceInfo: TraceInfo) { const {meta} = this.props; const errors = meta?.errors ?? traceInfo.errors.size; const performanceIssues = meta?.performance_issues ?? traceInfo.performanceIssues.size; return ( , fixed: '5 days ago', })} /> ); } renderTraceWarnings() { const {traces, orphanErrors} = this.props; const {roots, orphans} = (traces ?? []).reduce( (counts, trace) => { if (isRootTransaction(trace)) { counts.roots++; } else { counts.orphans++; } return counts; }, {roots: 0, orphans: 0} ); let warning: React.ReactNode = null; if (roots === 0 && orphans > 0) { warning = ( {t( 'A root transaction is missing. Transactions linked by a dashed line have been orphaned and cannot be directly linked to the root.' )} ); } else if (roots === 1 && orphans > 0) { warning = ( {t( 'This trace has broken subtraces. Transactions linked by a dashed line have been orphaned and cannot be directly linked to the root.' )} ); } else if (roots > 1) { warning = ( {t('Multiple root transactions have been found with this trace ID.')} ); } else if (orphanErrors && orphanErrors.length > 1) { 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: ( ), } )} ); } return warning; } renderContent() { const { dateSelected, isLoading, error, organization, location, traceEventView, traceSlug, traces, meta, orphanErrors, } = this.props; if (!dateSelected) { return this.renderTraceRequiresDateRangeSelection(); } if (isLoading) { return this.renderTraceLoading(); } const hasData = hasTraceData(traces, orphanErrors); if (error !== null || !hasData) { return ( ); } const traceInfo = traces ? getTraceInfo(traces, orphanErrors) : undefined; return ( {this.renderTraceWarnings()} {traceInfo && this.renderTraceHeader(traceInfo)} {this.renderSearchBar()} ); } render() { const {organization, location, traceEventView, traceSlug} = this.props; return ( {t('Trace ID: %s', traceSlug)} { trackAnalytics('performance_views.trace_view.open_in_discover', { organization, }); }} > {t('Open in Discover')} {this.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 Margin = styled('div')` margin-top: ${space(2)}; `; export default TraceDetailsContent;