import type React from 'react';
import {
Fragment,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useReducer,
useRef,
useState,
} from 'react';
import {browserHistory} from 'react-router';
import styled from '@emotion/styled';
import type {Location} from 'history';
import * as qs from 'query-string';
import ButtonBar from 'sentry/components/buttonBar';
import DiscoverButton from 'sentry/components/discoverButton';
import * as Layout from 'sentry/components/layouts/thirds';
import NoProjectMessage from 'sentry/components/noProjectMessage';
import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
import {t} from 'sentry/locale';
import type {EventTransaction, Organization} from 'sentry/types';
import {trackAnalytics} from 'sentry/utils/analytics';
import EventView from 'sentry/utils/discover/eventView';
import TraceMetaQuery, {
type TraceMetaQueryChildrenProps,
} from 'sentry/utils/performance/quickTrace/traceMetaQuery';
import type {
TraceFullDetailed,
TraceSplitResults,
} from 'sentry/utils/performance/quickTrace/types';
import {useApiQuery} from 'sentry/utils/queryClient';
import {decodeScalar} from 'sentry/utils/queryString';
import useApi from 'sentry/utils/useApi';
import {useLocation} from 'sentry/utils/useLocation';
import useOnClickOutside from 'sentry/utils/useOnClickOutside';
import useOrganization from 'sentry/utils/useOrganization';
import {useParams} from 'sentry/utils/useParams';
import useProjects from 'sentry/utils/useProjects';
import {rovingTabIndexReducer} from 'sentry/views/performance/newTraceDetails/rovingTabIndex';
import {
searchInTraceTree,
traceSearchReducer,
} from 'sentry/views/performance/newTraceDetails/traceSearch';
import {TraceSearchInput} from 'sentry/views/performance/newTraceDetails/traceSearchInput';
import {VirtualizedViewManager} from 'sentry/views/performance/newTraceDetails/virtualizedViewManager';
import Breadcrumb from '../breadcrumb';
import TraceDrawer from './traceDrawer/traceDrawer';
import {isTraceNode} from './guards';
import Trace from './trace';
import TraceHeader from './traceHeader';
import {TraceTree, type TraceTreeNode} from './traceTree';
import TraceWarnings from './traceWarnings';
import {useTrace} from './useTrace';
const DOCUMENT_TITLE = [t('Trace Details'), t('Performance')].join(' — ');
function maybeFocusRow() {
const focused_node = document.querySelector(".TraceRow[tabIndex='0']");
if (
focused_node &&
'focus' in focused_node &&
typeof focused_node.focus === 'function'
) {
focused_node.focus();
}
}
export function TraceView() {
const location = useLocation();
const organization = useOrganization();
const params = useParams<{traceSlug?: string}>();
const traceSlug = params.traceSlug?.trim() ?? '';
const queryParams = useMemo(() => {
const normalizedParams = normalizeDateTimeParams(location.query, {
allowAbsolutePageDatetime: true,
});
const start = decodeScalar(normalizedParams.start);
const end = decodeScalar(normalizedParams.end);
const statsPeriod = decodeScalar(normalizedParams.statsPeriod);
return {start, end, statsPeriod, useSpans: 1};
}, [location.query]);
const traceEventView = useMemo(() => {
const {start, end, statsPeriod} = queryParams;
return EventView.fromSavedQuery({
id: undefined,
name: `Events with Trace ID ${traceSlug}`,
fields: ['title', 'event.type', 'project', 'timestamp'],
orderby: '-timestamp',
query: `trace:${traceSlug}`,
projects: [ALL_ACCESS_PROJECTS],
version: 2,
start,
end,
range: statsPeriod,
});
}, [queryParams, traceSlug]);
const trace = useTrace();
return (
{metaResults => (
)}
);
}
type TraceViewContentProps = {
location: Location;
metaResults: TraceMetaQueryChildrenProps;
organization: Organization;
status: 'pending' | 'resolved' | 'error' | 'initial';
trace: TraceSplitResults | null;
traceEventView: EventView;
traceSlug: string;
};
function TraceViewContent(props: TraceViewContentProps) {
const api = useApi();
const [activeTab, setActiveTab] = useState<'trace' | 'node'>('trace');
const {projects} = useProjects();
const rootEvent = useRootEvent(props.trace);
const viewManager = useMemo(() => {
return new VirtualizedViewManager({
list: {width: 0.5},
span_list: {width: 0.5},
});
}, []);
const tree = useMemo(() => {
if (props.status === 'pending' || rootEvent.status !== 'success') {
return TraceTree.Loading({
project_slug: projects?.[0]?.slug ?? '',
event_id: props.traceSlug,
});
}
if (props.trace) {
return TraceTree.FromTrace(props.trace, rootEvent.data);
}
return TraceTree.Empty();
}, [
props.traceSlug,
props.trace,
props.status,
projects,
rootEvent.data,
rootEvent.status,
]);
const traceType = useMemo(() => {
if (props.status !== 'resolved' || !tree) {
return null;
}
return TraceTree.GetTraceType(tree.root);
}, [props.status, tree]);
const [rovingTabIndexState, rovingTabIndexDispatch] = useReducer(
rovingTabIndexReducer,
{
index: null,
items: null,
node: null,
}
);
useLayoutEffect(() => {
return rovingTabIndexDispatch({
type: 'initialize',
items: tree.list.length - 1,
index: null,
node: null,
});
}, [tree.list.length]);
const initialQuery = useMemo((): string | undefined => {
const query = qs.parse(location.search);
if (typeof query.search === 'string') {
return query.search;
}
return undefined;
// We only want to decode on load
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [searchState, searchDispatch] = useReducer(traceSearchReducer, {
query: initialQuery,
resultIteratorIndex: undefined,
resultIndex: undefined,
results: undefined,
status: undefined,
resultsLookup: new Map(),
});
const [clickedNode, setClickedNode] = useState[]>(
[]
);
const onSetClickedNode = useCallback(
(node: TraceTreeNode | null) => {
setActiveTab(node && !isTraceNode(node ?? null) ? 'node' : 'trace');
setClickedNode(node && !isTraceNode(node) ? [node] : []);
maybeFocusRow();
},
[]
);
const searchingRaf = useRef<{id: number | null} | null>(null);
const onTraceSearch = useCallback(
(query: string) => {
if (searchingRaf.current?.id) {
window.cancelAnimationFrame(searchingRaf.current.id);
}
searchingRaf.current = searchInTraceTree(query, tree, results => {
searchDispatch({
type: 'set results',
results: results[0],
resultsLookup: results[1],
});
});
},
[tree]
);
const previousResultIndexRef = useRef(searchState.resultIndex);
useLayoutEffect(() => {
if (previousResultIndexRef.current === searchState.resultIndex) {
return;
}
if (!viewManager.list) {
return;
}
if (typeof searchState.resultIndex !== 'number') {
return;
}
viewManager.list.scrollToRow(searchState.resultIndex);
previousResultIndexRef.current = searchState.resultIndex;
}, [searchState.resultIndex, viewManager.list]);
const onSearchChange = useCallback(
(event: React.ChangeEvent) => {
if (!event.currentTarget.value) {
searchDispatch({type: 'clear query'});
return;
}
onTraceSearch(event.currentTarget.value);
searchDispatch({type: 'set query', query: event.currentTarget.value});
},
[onTraceSearch]
);
const onSearchClear = useCallback(() => {
searchDispatch({type: 'clear query'});
}, []);
const onSearchKeyDown = useCallback((event: React.KeyboardEvent) => {
if (event.key === 'ArrowDown') {
searchDispatch({type: 'go to next match'});
} else {
if (event.key === 'ArrowUp') {
searchDispatch({type: 'go to previous match'});
}
}
}, []);
const onNextSearchClick = useCallback(() => {
searchDispatch({type: 'go to next match'});
}, []);
const onPreviousSearchClick = useCallback(() => {
searchDispatch({type: 'go to previous match'});
}, []);
const breadcrumbTransaction = useMemo(() => {
return {
project: rootEvent.data?.projectID ?? '',
name: rootEvent.data?.title ?? '',
};
}, [rootEvent.data]);
const trackOpenInDiscover = useCallback(() => {
trackAnalytics('performance_views.trace_view.open_in_discover', {
organization: props.organization,
});
}, [props.organization]);
const syncQuery = useMemo(() => {
return {search: searchState.query};
}, [searchState.query]);
useQueryParamSync(syncQuery);
const onOutsideClick = useCallback(() => {
const {node: _node, ...queryParamsWithoutNode} = qs.parse(location.search);
browserHistory.push({
pathname: location.pathname,
query: queryParamsWithoutNode,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const traceContainerRef = useRef(null);
useOnClickOutside(traceContainerRef, onOutsideClick);
const previouslyFocusedIndexRef = useRef(null);
const scrollToNode = useCallback(
(node: TraceTreeNode) => {
previouslyFocusedIndexRef.current = null;
viewManager
.scrollToPath(tree, [...node.path], () => void 0, {
api,
organization: props.organization,
})
.then(maybeNode => {
if (!maybeNode) {
return;
}
viewManager.onScrollEndOutOfBoundsCheck();
rovingTabIndexDispatch({
type: 'set index',
index: maybeNode.index,
node: maybeNode.node,
});
if (searchState.query) {
onTraceSearch(searchState.query);
}
maybeFocusRow();
});
},
[api, props.organization, tree, viewManager, searchState, onTraceSearch]
);
return (
{t('Trace ID: %s', props.traceSlug)}
{t('Open in Discover')}
{traceType ? : null}
(traceContainerRef.current = r)}>
);
}
function useQueryParamSync(query: Record) {
const previousQueryRef = useRef>(query);
const syncStateTimeoutRef = useRef(null);
useEffect(() => {
const keys = Object.keys(query);
const previousKeys = Object.keys(previousQueryRef.current);
if (
keys.length === previousKeys.length &&
keys.every(key => {
return query[key] === previousQueryRef.current[key];
})
) {
previousQueryRef.current = query;
return;
}
if (syncStateTimeoutRef.current !== null) {
window.clearTimeout(syncStateTimeoutRef.current);
}
previousQueryRef.current = query;
syncStateTimeoutRef.current = window.setTimeout(() => {
browserHistory.replace({
pathname: location.pathname,
query: {
...qs.parse(location.search),
...previousQueryRef.current,
},
});
}, 1000);
}, [query]);
}
function useRootEvent(trace: TraceSplitResults | null) {
const root = trace?.transactions[0] || trace?.orphan_errors[0];
const organization = useOrganization();
return useApiQuery(
[
`/organizations/${organization.slug}/events/${root?.project_slug}:${root?.event_id}/`,
{
query: {
referrer: 'trace-details-summary',
},
},
],
{
staleTime: 0,
enabled: !!trace,
}
);
}
const TraceBody = styled('div')`
padding-bottom: 0 !important;
padding-left: 24px;
padding-right: 24px;
padding-top: 24px;
background-color: ${p => p.theme.background};
display: flex;
flex-direction: column;
flex: 1 1 100%;
border-bottom: 2px solid red;
`;
const TraceTop = styled('div')`
display: flex;
flex-direction: column;
`;
const TraceLayout = styled('div')`
display: flex;
flex-direction: column;
flex: 1 1 100%;
border-bottom: 2px solid green;
~ footer {
display: none;
}
`;
const TraceContainer = styled('div')`
display: flex;
flex: 1 1 100%;
box-shadow: 0 0 0 1px ${p => p.theme.border};
border-bottom: 2px solid red;
`;
const TraceToolbar = styled('div')`
flex-grow: 0;
position: relative;
`;
const TraceGrid = styled('div')`
display: grid;
width: 100%;
grid-template-rows: 1fr min-content;
grid-template-columns: 100%;
`;