|
@@ -14,6 +14,7 @@ import styled from '@emotion/styled';
|
|
|
import * as Sentry from '@sentry/react';
|
|
|
import * as qs from 'query-string';
|
|
|
|
|
|
+import {addSuccessMessage} from 'sentry/actionCreators/indicator';
|
|
|
import {Button} from 'sentry/components/button';
|
|
|
import useFeedbackWidget from 'sentry/components/feedback/widget/useFeedbackWidget';
|
|
|
import LoadingIndicator from 'sentry/components/loadingIndicator';
|
|
@@ -21,7 +22,7 @@ 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 {t, tct} from 'sentry/locale';
|
|
|
import {space} from 'sentry/styles/space';
|
|
|
import type {EventTransaction} from 'sentry/types/event';
|
|
|
import type {Organization} from 'sentry/types/organization';
|
|
@@ -29,6 +30,7 @@ import type {Project} from 'sentry/types/project';
|
|
|
import {trackAnalytics} from 'sentry/utils/analytics';
|
|
|
import {browserHistory} from 'sentry/utils/browserHistory';
|
|
|
import EventView from 'sentry/utils/discover/eventView';
|
|
|
+import type {TraceSplitResults} from 'sentry/utils/performance/quickTrace/types';
|
|
|
import {
|
|
|
cancelAnimationTimeout,
|
|
|
requestAnimationTimeout,
|
|
@@ -81,8 +83,14 @@ import {
|
|
|
} from './traceState/traceStateProvider';
|
|
|
import {Trace} from './trace';
|
|
|
import {traceAnalytics} from './traceAnalytics';
|
|
|
-import {isTraceNode} from './traceGuards';
|
|
|
+import {
|
|
|
+ isAutogroupedNode,
|
|
|
+ isParentAutogroupedNode,
|
|
|
+ isSiblingAutogroupedNode,
|
|
|
+ isTraceNode,
|
|
|
+} from './traceGuards';
|
|
|
import {TraceMetadataHeader} from './traceMetadataHeader';
|
|
|
+import {TracePreferencesDropdown} from './tracePreferencesDropdown';
|
|
|
import {TraceShortcuts} from './traceShortcutsModal';
|
|
|
import type {TraceReducer, TraceReducerState} from './traceState';
|
|
|
import {
|
|
@@ -90,8 +98,8 @@ import {
|
|
|
traceNodeAnalyticsName,
|
|
|
} from './traceTreeAnalytics';
|
|
|
import TraceTypeWarnings from './traceTypeWarnings';
|
|
|
+import {useTraceOnLoad} from './useTraceOnLoad';
|
|
|
import {useTraceQueryParamStateSync} from './useTraceQueryParamStateSync';
|
|
|
-import {useTraceScrollToEventOnLoad} from './useTraceScrollToEventOnLoad';
|
|
|
import {useTraceScrollToPath} from './useTraceScrollToPath';
|
|
|
|
|
|
function logTraceMetadata(
|
|
@@ -181,16 +189,17 @@ export function TraceView() {
|
|
|
[]
|
|
|
);
|
|
|
|
|
|
- const metaResults = useTraceMeta([{traceSlug, timestamp: queryParams.timestamp}]);
|
|
|
- const traceResults = useTrace({traceSlug, timestamp: queryParams.timestamp});
|
|
|
- const tree = useTraceTree({traceSlug, traceResults, metaResults, replayRecord: null});
|
|
|
- const rootEvent = useTraceRootEvent(traceResults.data ?? null);
|
|
|
+ const meta = useTraceMeta([{traceSlug, timestamp: queryParams.timestamp}]);
|
|
|
+ const trace = useTrace({traceSlug, timestamp: queryParams.timestamp});
|
|
|
+ const rootEvent = useTraceRootEvent(trace.data ?? null);
|
|
|
+ const tree = useTraceTree({traceSlug, trace, meta, replay: null});
|
|
|
+
|
|
|
+ const title = useMemo(() => {
|
|
|
+ return `${t('Trace Details')} - ${traceSlug}`;
|
|
|
+ }, [traceSlug]);
|
|
|
|
|
|
return (
|
|
|
- <SentryDocumentTitle
|
|
|
- title={`${t('Trace Details')} - ${traceSlug}`}
|
|
|
- orgSlug={organization.slug}
|
|
|
- >
|
|
|
+ <SentryDocumentTitle title={title} orgSlug={organization.slug}>
|
|
|
<TraceStateProvider
|
|
|
initialPreferences={preferences}
|
|
|
preferencesStorageKey="trace-view-preferences"
|
|
@@ -205,13 +214,14 @@ export function TraceView() {
|
|
|
/>
|
|
|
<TraceInnerLayout>
|
|
|
<TraceViewWaterfall
|
|
|
- traceSlug={traceSlug}
|
|
|
tree={tree}
|
|
|
- organization={organization}
|
|
|
+ trace={trace}
|
|
|
+ meta={meta}
|
|
|
+ replay={null}
|
|
|
rootEvent={rootEvent}
|
|
|
+ traceSlug={traceSlug}
|
|
|
traceEventView={traceEventView}
|
|
|
- metaResults={metaResults}
|
|
|
- replayRecord={null}
|
|
|
+ organization={organization}
|
|
|
source="performance"
|
|
|
isEmbedded={false}
|
|
|
/>
|
|
@@ -235,11 +245,12 @@ const VITALS_TAB: TraceReducerState['tabs']['tabs'][0] = {
|
|
|
|
|
|
type TraceViewWaterfallProps = {
|
|
|
isEmbedded: boolean;
|
|
|
- metaResults: TraceMetaQueryResults;
|
|
|
+ meta: TraceMetaQueryResults;
|
|
|
organization: Organization;
|
|
|
- replayRecord: ReplayRecord | null;
|
|
|
+ replay: ReplayRecord | null;
|
|
|
rootEvent: UseApiQueryResult<EventTransaction, RequestError>;
|
|
|
source: string;
|
|
|
+ trace: UseApiQueryResult<TraceSplitResults<TraceTree.Transaction>, RequestError>;
|
|
|
traceEventView: EventView;
|
|
|
traceSlug: string | undefined;
|
|
|
tree: TraceTree;
|
|
@@ -253,16 +264,20 @@ type TraceViewWaterfallProps = {
|
|
|
|
|
|
export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
|
|
|
const api = useApi();
|
|
|
+ const filters = usePageFilters();
|
|
|
const {projects} = useProjects();
|
|
|
const organization = useOrganization();
|
|
|
- const [forceRender, rerender] = useReducer(x => (x + 1) % Number.MAX_SAFE_INTEGER, 0);
|
|
|
+
|
|
|
const traceState = useTraceState();
|
|
|
const traceDispatch = useTraceStateDispatch();
|
|
|
const traceStateEmitter = useTraceStateEmitter();
|
|
|
- const filters = usePageFilters();
|
|
|
- const traceScheduler = useMemo(() => new TraceScheduler(), []);
|
|
|
+
|
|
|
+ const [forceRender, rerender] = useReducer(x => (x + 1) % Number.MAX_SAFE_INTEGER, 0);
|
|
|
+
|
|
|
const traceView = useMemo(() => new TraceViewModel(), []);
|
|
|
+ const traceScheduler = useMemo(() => new TraceScheduler(), []);
|
|
|
|
|
|
+ const scrollQueueRef = useTraceScrollToPath(props.scrollToNode);
|
|
|
const forceRerender = useCallback(() => {
|
|
|
flushSync(rerender);
|
|
|
}, []);
|
|
@@ -282,7 +297,7 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
|
|
|
);
|
|
|
|
|
|
useEffect(() => {
|
|
|
- if (!props.replayTraces?.length || props.tree.type !== 'trace') {
|
|
|
+ if (!props.replayTraces?.length || props.tree?.type !== 'trace') {
|
|
|
return undefined;
|
|
|
}
|
|
|
|
|
@@ -293,7 +308,7 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
|
|
|
organization: props.organization,
|
|
|
urlParams: qs.parse(location.search),
|
|
|
rerender: forceRerender,
|
|
|
- metaResults: props.metaResults,
|
|
|
+ meta: props.meta,
|
|
|
});
|
|
|
|
|
|
return () => cleanup();
|
|
@@ -304,6 +319,11 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
|
|
|
const traceStateRef = useRef<TraceReducerState>(traceState);
|
|
|
traceStateRef.current = traceState;
|
|
|
|
|
|
+ const traceStatePreferencesRef = useRef<
|
|
|
+ Pick<TraceReducerState['preferences'], 'autogroup' | 'missing_instrumentation'>
|
|
|
+ >(traceState.preferences);
|
|
|
+ traceStatePreferencesRef.current = traceState.preferences;
|
|
|
+
|
|
|
// Initialize the view manager right after the state reducer
|
|
|
const viewManager = useMemo(() => {
|
|
|
return new VirtualizedViewManager(
|
|
@@ -440,7 +460,7 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
|
|
|
resultsLookup: lookup,
|
|
|
resultIteratorIndex: undefined,
|
|
|
resultIndex: undefined,
|
|
|
- previousNode: activeNodeSearchResult,
|
|
|
+ previousNode: null,
|
|
|
node: activeNode,
|
|
|
});
|
|
|
return;
|
|
@@ -449,6 +469,7 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
|
|
|
const resultIndex: number | undefined = matches?.[0]?.index;
|
|
|
const resultIteratorIndex: number | undefined = matches?.[0] ? 0 : undefined;
|
|
|
const node: TraceTreeNode<TraceTree.NodeValue> | null = matches?.[0]?.value;
|
|
|
+
|
|
|
traceDispatch({
|
|
|
type: 'set results',
|
|
|
results: matches,
|
|
@@ -605,6 +626,7 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
|
|
|
return TraceTree.ExpandToPath(props.tree, TraceTree.PathToNode(node), {
|
|
|
api,
|
|
|
organization: props.organization,
|
|
|
+ preferences: traceStatePreferencesRef.current,
|
|
|
}).then(() => {
|
|
|
const maybeNode = TraceTree.Find(props.tree.root, n => n === node);
|
|
|
|
|
@@ -612,22 +634,9 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- let index = props.tree.list.indexOf(maybeNode);
|
|
|
-
|
|
|
- if (node && index === -1) {
|
|
|
- let parent_node = node.parent;
|
|
|
- while (parent_node) {
|
|
|
- // Transactions break autogrouping chains, so we can stop here
|
|
|
- props.tree.expand(parent_node, true);
|
|
|
- // This is very wasteful as it performs O(n^2) search each time we expand a node...
|
|
|
- // In most cases though, we should be operating on a tree with sub 10k elements and hopefully
|
|
|
- // a low autogrouped node count.
|
|
|
- index = node ? props.tree.list.findIndex(n => n === node) : -1;
|
|
|
- if (index !== -1) {
|
|
|
- break;
|
|
|
- }
|
|
|
- parent_node = parent_node.parent;
|
|
|
- }
|
|
|
+ const index = TraceTree.EnforceVisibility(props.tree, maybeNode);
|
|
|
+ if (index === -1) {
|
|
|
+ return null;
|
|
|
}
|
|
|
|
|
|
scrollRowIntoView(maybeNode, index, 'center if outside', true);
|
|
@@ -679,62 +688,101 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
|
|
|
// Callback that is invoked when the trace loads and reaches its initialied state,
|
|
|
// that is when the trace tree data and any data that the trace depends on is loaded,
|
|
|
// but the trace is not yet rendered in the view.
|
|
|
- const onTraceLoad = useCallback(
|
|
|
- (
|
|
|
- _trace: TraceTree,
|
|
|
- nodeToScrollTo: TraceTreeNode<TraceTree.NodeValue> | null,
|
|
|
- indexOfNodeToScrollTo: number | null
|
|
|
- ) => {
|
|
|
- const query = qs.parse(location.search);
|
|
|
+ const onTraceLoad = useCallback(() => {
|
|
|
+ // The tree has the data fetched, but does not yet respect the user preferences.
|
|
|
+ // We will autogroup and inject missing instrumentation if the preferences are set.
|
|
|
+ // and then we will perform a search to find the node the user is interested in.
|
|
|
+ const query = qs.parse(location.search);
|
|
|
+ if (query.fov && typeof query.fov === 'string') {
|
|
|
+ viewManager.maybeInitializeTraceViewFromQS(query.fov);
|
|
|
+ }
|
|
|
+ if (traceStateRef.current.preferences.missing_instrumentation) {
|
|
|
+ TraceTree.DetectMissingInstrumentation(props.tree.root);
|
|
|
+ }
|
|
|
+ if (traceStateRef.current.preferences.autogroup.sibling) {
|
|
|
+ TraceTree.AutogroupSiblingSpanNodes(props.tree.root);
|
|
|
+ }
|
|
|
+ if (traceStateRef.current.preferences.autogroup.parent) {
|
|
|
+ TraceTree.AutogroupDirectChildrenSpanNodes(props.tree.root);
|
|
|
+ }
|
|
|
|
|
|
- if (query.fov && typeof query.fov === 'string') {
|
|
|
- viewManager.maybeInitializeTraceViewFromQS(query.fov);
|
|
|
+ // Construct the visual representation of the tree
|
|
|
+ props.tree.build();
|
|
|
+
|
|
|
+ const eventId = scrollQueueRef.current?.eventId;
|
|
|
+ const [type, path] = scrollQueueRef.current?.path?.[0]?.split('-') ?? [];
|
|
|
+ scrollQueueRef.current = null;
|
|
|
+
|
|
|
+ let node =
|
|
|
+ (path === 'root' && props.tree.root.children[0]) ||
|
|
|
+ (path && TraceTree.FindByID(props.tree.root, path)) ||
|
|
|
+ (eventId && TraceTree.FindByID(props.tree.root, eventId)) ||
|
|
|
+ null;
|
|
|
+
|
|
|
+ // If the node points to a span, but we found an autogrouped node, then
|
|
|
+ // perform another search inside the autogrouped node to find the more detailed
|
|
|
+ // location of the span. This is necessary because the id of the autogrouped node
|
|
|
+ // is in some cases inferred from the spans it contains and searching by the span id
|
|
|
+ // just gives us the first match which may not be the one the user is looking for.
|
|
|
+ if (node) {
|
|
|
+ if (isAutogroupedNode(node) && type !== 'ag') {
|
|
|
+ if (isParentAutogroupedNode(node)) {
|
|
|
+ node = TraceTree.FindByID(node.head, eventId ?? path) ?? node;
|
|
|
+ } else if (isSiblingAutogroupedNode(node)) {
|
|
|
+ node = node.children.find(n => TraceTree.FindByID(n, eventId ?? path)) ?? node;
|
|
|
+ }
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- if (nodeToScrollTo !== null && indexOfNodeToScrollTo !== null) {
|
|
|
- // At load time, we want to scroll the row into view, but we need to wait for the view
|
|
|
- // to initialize before we can do that. We listen for the 'initialize virtualized list' and scroll
|
|
|
- // to the row in the view if it is not in view yet. If its in the view, then scroll to it immediately.
|
|
|
- traceScheduler.once('initialize virtualized list', () => {
|
|
|
- function onTargetRowMeasure() {
|
|
|
- if (!nodeToScrollTo || !viewManager.row_measurer.cache.has(nodeToScrollTo)) {
|
|
|
- return;
|
|
|
- }
|
|
|
- viewManager.row_measurer.off('row measure end', onTargetRowMeasure);
|
|
|
- if (viewManager.isOutsideOfView(nodeToScrollTo)) {
|
|
|
- viewManager.scrollRowIntoViewHorizontally(
|
|
|
- nodeToScrollTo!,
|
|
|
- 0,
|
|
|
- 48,
|
|
|
- 'measured'
|
|
|
- );
|
|
|
- }
|
|
|
- }
|
|
|
- viewManager.scrollToRow(indexOfNodeToScrollTo, 'center');
|
|
|
- viewManager.row_measurer.on('row measure end', onTargetRowMeasure);
|
|
|
- previouslyScrolledToNodeRef.current = nodeToScrollTo;
|
|
|
+ const index = node ? TraceTree.EnforceVisibility(props.tree, node) : -1;
|
|
|
|
|
|
- setRowAsFocused(
|
|
|
- nodeToScrollTo,
|
|
|
- null,
|
|
|
- traceStateRef.current.search.resultsLookup,
|
|
|
- indexOfNodeToScrollTo
|
|
|
- );
|
|
|
- traceDispatch({
|
|
|
- type: 'set roving index',
|
|
|
- node: nodeToScrollTo,
|
|
|
- index: indexOfNodeToScrollTo,
|
|
|
- action_source: 'load',
|
|
|
- });
|
|
|
- });
|
|
|
- }
|
|
|
+ if (traceStateRef.current.search.query) {
|
|
|
+ onTraceSearch(traceStateRef.current.search.query, node, 'persist');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (index === -1 || !node) {
|
|
|
+ Sentry.withScope(scope => {
|
|
|
+ scope.setFingerprint(['trace-view-scroll-to-node-error']);
|
|
|
+ scope.captureMessage('Failed to scroll to node in trace tree');
|
|
|
+ });
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- if (traceStateRef.current.search.query) {
|
|
|
- onTraceSearch(traceStateRef.current.search.query, nodeToScrollTo, 'persist');
|
|
|
+ // At load time, we want to scroll the row into view, but we need to wait for the view
|
|
|
+ // to initialize before we can do that. We listen for the 'initialize virtualized list' and scroll
|
|
|
+ // to the row in the view if it is not in view yet. If its in the view, then scroll to it immediately.
|
|
|
+ traceScheduler.once('initialize virtualized list', () => {
|
|
|
+ function onTargetRowMeasure() {
|
|
|
+ if (!node || !viewManager.row_measurer.cache.has(node)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ viewManager.row_measurer.off('row measure end', onTargetRowMeasure);
|
|
|
+ if (viewManager.isOutsideOfView(node)) {
|
|
|
+ viewManager.scrollRowIntoViewHorizontally(node!, 0, 48, 'measured');
|
|
|
+ }
|
|
|
}
|
|
|
- },
|
|
|
- [setRowAsFocused, traceDispatch, onTraceSearch, viewManager, traceScheduler]
|
|
|
- );
|
|
|
+ viewManager.scrollToRow(index, 'center');
|
|
|
+ viewManager.row_measurer.on('row measure end', onTargetRowMeasure);
|
|
|
+ previouslyScrolledToNodeRef.current = node;
|
|
|
+
|
|
|
+ setRowAsFocused(node, null, traceStateRef.current.search.resultsLookup, index);
|
|
|
+ traceDispatch({
|
|
|
+ type: 'set roving index',
|
|
|
+ node: node,
|
|
|
+ index: index,
|
|
|
+ action_source: 'load',
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }, [
|
|
|
+ setRowAsFocused,
|
|
|
+ traceDispatch,
|
|
|
+ onTraceSearch,
|
|
|
+ viewManager,
|
|
|
+ traceScheduler,
|
|
|
+ props.tree,
|
|
|
+ scrollQueueRef,
|
|
|
+ ]);
|
|
|
|
|
|
// Setup the middleware for the trace reducer
|
|
|
useLayoutEffect(() => {
|
|
@@ -855,27 +903,89 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
|
|
|
};
|
|
|
}, [viewManager, traceScheduler, props.tree]);
|
|
|
|
|
|
+ const onLoadScrollStatus = useTraceOnLoad({
|
|
|
+ onTraceLoad,
|
|
|
+ pathToNodeOrEventId: scrollQueueRef.current,
|
|
|
+ tree: props.tree,
|
|
|
+ });
|
|
|
+
|
|
|
// Sync part of the state with the URL
|
|
|
const traceQueryStateSync = useMemo(() => {
|
|
|
return {search: traceState.search.query};
|
|
|
}, [traceState.search.query]);
|
|
|
useTraceQueryParamStateSync(traceQueryStateSync);
|
|
|
|
|
|
- const scrollQueueRef = useTraceScrollToPath(props.scrollToNode);
|
|
|
+ const onAutogroupChange = useCallback(() => {
|
|
|
+ const value = !traceState.preferences.autogroup.parent;
|
|
|
+
|
|
|
+ if (!value) {
|
|
|
+ let removeCount = 0;
|
|
|
+ removeCount += TraceTree.RemoveSiblingAutogroupNodes(props.tree.root);
|
|
|
+ removeCount += TraceTree.RemoveDirectChildrenAutogroupNodes(props.tree.root);
|
|
|
+
|
|
|
+ addSuccessMessage(
|
|
|
+ removeCount > 0
|
|
|
+ ? tct('Autogrouping disabled, removed [count] autogroup spans', {
|
|
|
+ count: removeCount,
|
|
|
+ })
|
|
|
+ : t('Autogrouping disabled')
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ let autogroupCount = 0;
|
|
|
+ autogroupCount += TraceTree.AutogroupSiblingSpanNodes(props.tree.root);
|
|
|
+ autogroupCount += TraceTree.AutogroupDirectChildrenSpanNodes(props.tree.root);
|
|
|
+ addSuccessMessage(
|
|
|
+ autogroupCount > 0
|
|
|
+ ? tct('Autogrouping enabled, detected [count] autogrouping spans', {
|
|
|
+ count: autogroupCount,
|
|
|
+ })
|
|
|
+ : t('Autogrouping enabled')
|
|
|
+ );
|
|
|
+ }
|
|
|
|
|
|
- useTraceScrollToEventOnLoad({
|
|
|
- rerender,
|
|
|
- onTraceLoad,
|
|
|
- scrollQueueRef,
|
|
|
- scheduler: traceScheduler,
|
|
|
- trace: props.tree,
|
|
|
- });
|
|
|
+ props.tree.rebuild();
|
|
|
+ traceDispatch({
|
|
|
+ type: 'set autogrouping',
|
|
|
+ payload: value,
|
|
|
+ });
|
|
|
+ }, [traceDispatch, traceState.preferences.autogroup, props.tree]);
|
|
|
+
|
|
|
+ const onMissingInstrumentationChange = useCallback(() => {
|
|
|
+ const value = !traceState.preferences.missing_instrumentation;
|
|
|
+ if (!value) {
|
|
|
+ const removeCount = TraceTree.RemoveMissingInstrumentationNodes(props.tree.root);
|
|
|
+ addSuccessMessage(
|
|
|
+ removeCount > 0
|
|
|
+ ? tct(
|
|
|
+ 'Missing instrumentation disabled, removed [count] missing instrumentation spans',
|
|
|
+ {
|
|
|
+ count: removeCount,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ : t('Missing instrumentation disabled')
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ const missingInstrumentationCount = TraceTree.DetectMissingInstrumentation(
|
|
|
+ props.tree.root
|
|
|
+ );
|
|
|
+ addSuccessMessage(
|
|
|
+ missingInstrumentationCount > 0
|
|
|
+ ? tct(
|
|
|
+ 'Missing instrumentation enabled, found [count] missing instrumentation spans',
|
|
|
+ {
|
|
|
+ count: missingInstrumentationCount,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ : t('Missing instrumentation enabled')
|
|
|
+ );
|
|
|
+ }
|
|
|
|
|
|
- const isLoading = !!(
|
|
|
- props.tree.type === 'loading' ||
|
|
|
- props.tree.type !== 'trace' ||
|
|
|
- scrollQueueRef.current
|
|
|
- );
|
|
|
+ props.tree.rebuild();
|
|
|
+ traceDispatch({
|
|
|
+ type: 'set missing instrumentation',
|
|
|
+ payload: value,
|
|
|
+ });
|
|
|
+ }, [traceDispatch, traceState.preferences.missing_instrumentation, props.tree]);
|
|
|
|
|
|
return (
|
|
|
<Fragment>
|
|
@@ -891,6 +1001,15 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
|
|
|
organization={props.organization}
|
|
|
/>
|
|
|
<TraceShortcuts />
|
|
|
+ <TracePreferencesDropdown
|
|
|
+ autogroup={
|
|
|
+ traceState.preferences.autogroup.parent &&
|
|
|
+ traceState.preferences.autogroup.sibling
|
|
|
+ }
|
|
|
+ missingInstrumentation={traceState.preferences.missing_instrumentation}
|
|
|
+ onAutogroupChange={onAutogroupChange}
|
|
|
+ onMissingInstrumentationChange={onMissingInstrumentationChange}
|
|
|
+ />
|
|
|
</TraceToolbar>
|
|
|
<TraceGrid layout={traceState.preferences.layout} ref={setTraceGridRef}>
|
|
|
<Trace
|
|
@@ -904,20 +1023,20 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
|
|
|
scheduler={traceScheduler}
|
|
|
forceRerender={forceRender}
|
|
|
isEmbedded={props.isEmbedded}
|
|
|
- isLoading={isLoading}
|
|
|
+ isLoading={props.tree.type === 'loading' || onLoadScrollStatus === 'pending'}
|
|
|
/>
|
|
|
|
|
|
- {props.tree.type === 'error' ? (
|
|
|
+ {props.tree.type === 'loading' || onLoadScrollStatus === 'pending' ? (
|
|
|
+ <TraceLoading />
|
|
|
+ ) : props.tree.type === 'error' ? (
|
|
|
<TraceError />
|
|
|
) : props.tree.type === 'empty' ? (
|
|
|
<TraceEmpty />
|
|
|
- ) : isLoading ? (
|
|
|
- <TraceLoading />
|
|
|
) : null}
|
|
|
|
|
|
<TraceDrawer
|
|
|
- replayRecord={props.replayRecord}
|
|
|
- metaResults={props.metaResults}
|
|
|
+ replay={props.replay}
|
|
|
+ meta={props.meta}
|
|
|
traceType={shape}
|
|
|
trace={props.tree}
|
|
|
traceGridRef={traceGridRef}
|
|
@@ -984,7 +1103,7 @@ const TraceInnerLayout = styled('div')`
|
|
|
const TraceToolbar = styled('div')`
|
|
|
flex-grow: 0;
|
|
|
display: grid;
|
|
|
- grid-template-columns: 1fr min-content min-content;
|
|
|
+ grid-template-columns: 1fr min-content min-content min-content;
|
|
|
gap: ${space(1)};
|
|
|
`;
|
|
|
|