import {Component, createRef, Fragment} from 'react'; import {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 {Organization} from 'sentry/types'; import {defined} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import EventView from 'sentry/utils/discover/eventView'; import {QueryError} from 'sentry/utils/discover/genericDiscoverQuery'; import {getDuration} from 'sentry/utils/formatters'; import {createFuzzySearch, Fuse} from 'sentry/utils/fuzzySearch'; import getDynamicText from 'sentry/utils/getDynamicText'; import {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 {TraceInfo} from './types'; import {getTraceInfo, isRootTransaction} from './utils'; type IndexedFusedTransaction = { indexed: string[]; transaction: TraceFullDetailed; }; type Props = Pick, 'params' | 'location'> & { dateSelected: boolean; error: QueryError | null; isLoading: boolean; meta: TraceMeta | null; organization: Organization; traceEventView: EventView; traceSlug: string; traces: TraceFullDetailed[] | null; }; type State = { filteredTransactionIds: Set | undefined; searchQuery: string | undefined; }; class TraceDetailsContent extends Component { state: State = { searchQuery: undefined, filteredTransactionIds: undefined, }; componentDidMount() { this.initFuse(); } componentDidUpdate(prevProps: Props) { if (this.props.traces !== prevProps.traces) { this.initFuse(); } } fuse: Fuse | null = null; traceViewRef = createRef(); virtualScrollbarContainerRef = createRef(); async initFuse() { if (defined(this.props.traces) && this.props.traces.length > 0) { const transformed: IndexedFusedTransaction[] = this.props.traces.flatMap(trace => reduceTrace( trace, (acc, transaction) => { const indexed: string[] = [ transaction['transaction.op'], transaction.transaction, transaction.project_slug, ]; acc.push({ transaction, indexed, }); return acc; }, [] ) ); this.fuse = await createFuzzySearch(transformed, { keys: ['indexed'], includeMatches: true, threshold: 0.6, location: 0, distance: 100, maxPatternLength: 32, }); } } renderTraceLoading() { return ; } renderTraceRequiresDateRangeSelection() { return ; } handleTransactionFilter = (searchQuery: string) => { this.setState({searchQuery: searchQuery || undefined}, this.filterTransactions); }; filterTransactions = () => { const {traces} = this.props; const {filteredTransactionIds, searchQuery} = this.state; if (!searchQuery || traces === null || traces.length <= 0 || !defined(this.fuse)) { if (filteredTransactionIds !== undefined) { this.setState({ filteredTransactionIds: 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.transaction.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 = traces .flatMap(trace => filterTrace( trace, ({event_id, span_id}) => event_id.includes(searchQuery) || span_id.includes(searchQuery) ) ) .map(transaction => transaction.event_id); this.setState({ filteredTransactionIds: 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} = 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.')} ); } return warning; } renderContent() { const { dateSelected, isLoading, error, organization, location, traceEventView, traceSlug, traces, meta, } = this.props; if (!dateSelected) { return this.renderTraceRequiresDateRangeSelection(); } if (isLoading) { return this.renderTraceLoading(); } if (error !== null || traces === null || traces.length <= 0) { return ( ); } const traceInfo = getTraceInfo(traces); return ( {this.renderTraceWarnings()} {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 Margin = styled('div')` margin-top: ${space(2)}; `; export default TraceDetailsContent;