diff --git a/static/app/views/performance/newTraceDetails/index.tsx b/static/app/views/performance/newTraceDetails/index.tsx index 0a0615a6c75908..0036b0497dc912 100644 --- a/static/app/views/performance/newTraceDetails/index.tsx +++ b/static/app/views/performance/newTraceDetails/index.tsx @@ -240,7 +240,7 @@ const TraceExternalLayout = styled('div')` const FlexBox = styled('div')` display: flex; flex-direction: column; - gap: ${space(2)}; + gap: ${space(1)}; `; const TraceInnerLayout = styled(FlexBox)` diff --git a/static/app/views/performance/newTraceDetails/traceActionsMenu.tsx b/static/app/views/performance/newTraceDetails/traceActionsMenu.tsx index 3d00fe41519ad7..3046e0aa6329c2 100644 --- a/static/app/views/performance/newTraceDetails/traceActionsMenu.tsx +++ b/static/app/views/performance/newTraceDetails/traceActionsMenu.tsx @@ -3,7 +3,6 @@ import {Button} from 'sentry/components/core/button'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; import {IconEllipsis, IconOpen} from 'sentry/icons'; import {t} from 'sentry/locale'; -import type {Project} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; import type EventView from 'sentry/utils/discover/eventView'; import {SavedQueryDatasets} from 'sentry/utils/discover/types'; @@ -12,12 +11,11 @@ import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; import useProjects from 'sentry/utils/useProjects'; import {hasDatasetSelector} from 'sentry/views/dashboards/utils'; -import {OurLogKnownFieldKey} from 'sentry/views/explore/logs/types'; -import {isTraceItemDetailsResponse} from 'sentry/views/performance/newTraceDetails/traceApi/utils'; +import {getTraceProject} from 'sentry/views/performance/newTraceDetails/tracePreferencesDropdown'; +import {useHasTraceTabsUI} from 'sentry/views/performance/newTraceDetails/useHasTraceTabsUI'; import type {TraceRootEventQueryResults} from './traceApi/useTraceRootEvent'; import { - findSpanAttributeValue, getSearchInExploreTarget, TraceDrawerActionKind, } from './traceDrawer/details/utils'; @@ -39,22 +37,14 @@ function TraceActionsMenu({ const {projects} = useProjects(); const navigate = useNavigate(); const hasExploreEnabled = organization.features.includes('visibility-explore-view'); + const hasTraceTabsUi = useHasTraceTabsUI(); - let traceProject: Project | undefined; - if (rootEventResults.data) { - if (isTraceItemDetailsResponse(rootEventResults.data)) { - const attributes = rootEventResults.data.attributes; - const projectId = - OurLogKnownFieldKey.PROJECT_ID in attributes - ? attributes[OurLogKnownFieldKey.PROJECT_ID] - : findSpanAttributeValue(attributes, 'project_id'); - traceProject = projects.find(p => p.id === projectId); - } else { - const projectSlug = rootEventResults.data.projectSlug; - traceProject = projects.find(p => p.slug === projectSlug); - } + if (hasTraceTabsUi) { + return null; } + const traceProject = getTraceProject(projects, rootEventResults); + return ( <DropdownMenu items={[ diff --git a/static/app/views/performance/newTraceDetails/traceOpenInExploreButton.tsx b/static/app/views/performance/newTraceDetails/traceOpenInExploreButton.tsx new file mode 100644 index 00000000000000..2cb040e5ae71a2 --- /dev/null +++ b/static/app/views/performance/newTraceDetails/traceOpenInExploreButton.tsx @@ -0,0 +1,65 @@ +import {Button} from 'sentry/components/core/button'; +import {t} from 'sentry/locale'; +import type EventView from 'sentry/utils/discover/eventView'; +import {useLocation} from 'sentry/utils/useLocation'; +import {useNavigate} from 'sentry/utils/useNavigate'; +import useOrganization from 'sentry/utils/useOrganization'; +import {traceAnalytics} from 'sentry/views/performance/newTraceDetails/traceAnalytics'; +import { + getSearchInExploreTarget, + TraceDrawerActionKind, +} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/utils'; +import {useHasTraceTabsUI} from 'sentry/views/performance/newTraceDetails/useHasTraceTabsUI'; + +type Props = { + traceEventView: EventView; + trace_id: string; +}; + +export function TraceOpenInExploreButton({trace_id, traceEventView}: Props) { + const organization = useOrganization(); + const hasExploreEnabled = organization.features.includes('visibility-explore-view'); + const hasTraceTabsUi = useHasTraceTabsUI(); + const location = useLocation(); + const navigate = useNavigate(); + + if (!hasExploreEnabled || !hasTraceTabsUi || !trace_id) { + return null; + } + + return ( + <Button + size="xs" + onClick={() => { + traceAnalytics.trackExploreSearch( + organization, + 'trace', + trace_id ?? '', + TraceDrawerActionKind.INCLUDE, + 'toolbar_menu' + ); + + const {start, end, statsPeriod} = traceEventView; + const target = getSearchInExploreTarget( + organization, + { + ...location, + query: { + start, + end, + statsPeriod: start && end ? null : statsPeriod, // We don't want statsPeriod to have precedence over start and end + }, + }, + '-1', + 'trace', + trace_id ?? '', + TraceDrawerActionKind.INCLUDE + ); + + navigate(target); + }} + > + {t('Open in Explore')} + </Button> + ); +} diff --git a/static/app/views/performance/newTraceDetails/tracePreferencesDropdown.tsx b/static/app/views/performance/newTraceDetails/tracePreferencesDropdown.tsx index 0267a68fecb94f..a98e6492949c55 100644 --- a/static/app/views/performance/newTraceDetails/tracePreferencesDropdown.tsx +++ b/static/app/views/performance/newTraceDetails/tracePreferencesDropdown.tsx @@ -1,9 +1,22 @@ import {useCallback, useMemo} from 'react'; +import {openModal} from 'sentry/actionCreators/modal'; import {CompactSelect, type SelectOption} from 'sentry/components/core/compactSelect'; import type {DropdownButtonProps} from 'sentry/components/dropdownButton'; +import ExternalLink from 'sentry/components/links/externalLink'; import {IconSettings} from 'sentry/icons'; -import {t} from 'sentry/locale'; +import {t, tct} from 'sentry/locale'; +import type {Project} from 'sentry/types/project'; +import useOrganization from 'sentry/utils/useOrganization'; +import useProjects from 'sentry/utils/useProjects'; +import {OurLogKnownFieldKey} from 'sentry/views/explore/logs/types'; +import {traceAnalytics} from 'sentry/views/performance/newTraceDetails/traceAnalytics'; +import type {TraceRootEventQueryResults} from 'sentry/views/performance/newTraceDetails/traceApi/useTraceRootEvent'; +import {isTraceItemDetailsResponse} from 'sentry/views/performance/newTraceDetails/traceApi/utils'; +import {getCustomInstrumentationLink} from 'sentry/views/performance/newTraceDetails/traceConfigurations'; +import {findSpanAttributeValue} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/utils'; +import {TraceShortcutsModal} from 'sentry/views/performance/newTraceDetails/traceShortcutsModal'; +import {useHasTraceTabsUI} from 'sentry/views/performance/newTraceDetails/useHasTraceTabsUI'; const CompactSelectTriggerProps: DropdownButtonProps = { icon: <IconSettings />, @@ -17,27 +30,36 @@ interface TracePreferencesDropdownProps { missingInstrumentation: boolean; onAutogroupChange: () => void; onMissingInstrumentationChange: () => void; + rootEventResults: TraceRootEventQueryResults; } -const TRACE_PREFERENCES_DROPDOWN_OPTIONS: Array<SelectOption<string>> = [ - { - label: t('Autogrouping'), - value: 'autogroup', - details: t( - 'Collapses 5 or more sibling spans with the same description or any spans with 2 or more descendants with the same operation.' - ), - }, - { - label: t('No Instrumentation'), - value: 'no-instrumentation', - details: t( - 'Shows when there is more than 100ms of unaccounted elapsed time between two spans.' - ), - }, -]; - export function TracePreferencesDropdown(props: TracePreferencesDropdownProps) { - const values = useMemo(() => { + const hasTraceTabsUi = useHasTraceTabsUI(); + const organization = useOrganization(); + const {projects} = useProjects(); + + const traceProject = getTraceProject(projects, props.rootEventResults); + const selectOptions: Array<SelectOption<string>> = [ + { + label: t('Autogrouping'), + value: 'autogroup', + details: t( + 'Collapses 5 or more sibling spans with the same description or any spans with 2 or more descendants with the same operation.' + ), + }, + { + label: t('No Instrumentation'), + value: 'no-instrumentation', + details: tct( + 'Shows when there is more than 100ms of unaccounted elapsed time between two spans.[link: Go to docs to instrument more.]', + { + link: <ExternalLink href={getCustomInstrumentationLink(traceProject)} />, + } + ), + }, + ]; + + const values: string[] = useMemo(() => { const value: string[] = []; if (props.autogroup) { value.push('autogroup'); @@ -78,6 +100,17 @@ export function TracePreferencesDropdown(props: TracePreferencesDropdownProps) { [values, onAutogroupChange, onMissingInstrumentationChange] ); + const menuFooter = hasTraceTabsUi ? ( + <a + onClick={() => { + traceAnalytics.trackViewShortcuts(organization); + openModal(p => <TraceShortcutsModal {...p} />); + }} + > + {t('See Shortcuts')} + </a> + ) : null; + return ( <CompactSelect multiple @@ -85,9 +118,31 @@ export function TracePreferencesDropdown(props: TracePreferencesDropdownProps) { // Force the trigger to be so that we only render the icon triggerLabel="" triggerProps={CompactSelectTriggerProps} - options={TRACE_PREFERENCES_DROPDOWN_OPTIONS} + options={selectOptions} onChange={onChange} + menuFooter={menuFooter} menuWidth={300} /> ); } + +export function getTraceProject( + projects: Project[], + rootEventResults: TraceRootEventQueryResults +): Project | undefined { + if (!rootEventResults.data) { + return undefined; + } + + if (isTraceItemDetailsResponse(rootEventResults.data)) { + const attributes = rootEventResults.data.attributes; + const projectId = + OurLogKnownFieldKey.PROJECT_ID in attributes + ? attributes[OurLogKnownFieldKey.PROJECT_ID] + : findSpanAttributeValue(attributes, 'project_id'); + return projects.find(p => p.id === projectId); + } + + const projectSlug = rootEventResults.data.projectSlug; + return projects.find(p => p.slug === projectSlug); +} diff --git a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx index dad7b7dab4194a..12e1e65e201246 100644 --- a/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx +++ b/static/app/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager.tsx @@ -275,6 +275,7 @@ export class VirtualizedViewManager { registerResetZoomRef(ref: HTMLButtonElement | null) { this.reset_zoom_button = ref; + this.syncResetZoomButton(); } registerGhostRowRef(column: string, ref: HTMLElement | null) { diff --git a/static/app/views/performance/newTraceDetails/traceResetZoomButton.tsx b/static/app/views/performance/newTraceDetails/traceResetZoomButton.tsx index 1702c5d6fc94d7..c24a1288c2903c 100644 --- a/static/app/views/performance/newTraceDetails/traceResetZoomButton.tsx +++ b/static/app/views/performance/newTraceDetails/traceResetZoomButton.tsx @@ -6,11 +6,13 @@ import {t} from 'sentry/locale'; import type {Organization} from 'sentry/types/organization'; import {traceAnalytics} from 'sentry/views/performance/newTraceDetails/traceAnalytics'; import type {VirtualizedViewManager} from 'sentry/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager'; +import {useHasTraceTabsUI} from 'sentry/views/performance/newTraceDetails/useHasTraceTabsUI'; export function TraceResetZoomButton(props: { organization: Organization; viewManager: VirtualizedViewManager; }) { + const hasTraceTabsUi = useHasTraceTabsUI(); const onResetZoom = useCallback(() => { traceAnalytics.trackResetZoom(props.organization); props.viewManager.resetZoom(); @@ -18,6 +20,7 @@ export function TraceResetZoomButton(props: { return ( <ResetZoomButton + hide={hasTraceTabsUi && props.viewManager.reset_zoom_button?.disabled !== false} size="xs" onClick={onResetZoom} ref={props.viewManager.registerResetZoomRef} @@ -27,8 +30,10 @@ export function TraceResetZoomButton(props: { ); } -const ResetZoomButton = styled(Button)` - transition: opacity 0.2s 0.5s ease-in-out; +const ResetZoomButton = styled(Button)<{ + hide: boolean; +}>` + display: ${props => (props.hide ? 'none' : 'block')}; &[disabled] { cursor: not-allowed; diff --git a/static/app/views/performance/newTraceDetails/traceWaterfall.tsx b/static/app/views/performance/newTraceDetails/traceWaterfall.tsx index f561ee0b43125e..720bf6a7576f49 100644 --- a/static/app/views/performance/newTraceDetails/traceWaterfall.tsx +++ b/static/app/views/performance/newTraceDetails/traceWaterfall.tsx @@ -38,6 +38,7 @@ import type {TraceRootEventQueryResults} from 'sentry/views/performance/newTrace import {isTraceItemDetailsResponse} from 'sentry/views/performance/newTraceDetails/traceApi/utils'; import {TraceLinkNavigationButton} from 'sentry/views/performance/newTraceDetails/traceLinksNavigation/traceLinkNavigationButton'; import {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree'; +import {TraceOpenInExploreButton} from 'sentry/views/performance/newTraceDetails/traceOpenInExploreButton'; import {useDividerResizeSync} from 'sentry/views/performance/newTraceDetails/useDividerResizeSync'; import {useHasTraceTabsUI} from 'sentry/views/performance/newTraceDetails/useHasTraceTabsUI'; import {useTraceSpaceListeners} from 'sentry/views/performance/newTraceDetails/useTraceSpaceListeners'; @@ -774,6 +775,10 @@ export function TraceWaterfall(props: TraceWaterfallProps) { /> <TraceToolbar> <TraceSearchInput onTraceSearch={onTraceSearch} organization={organization} /> + <TraceOpenInExploreButton + trace_id={props.traceSlug} + traceEventView={props.traceEventView} + /> <TraceResetZoomButton viewManager={viewManager} organization={props.organization} @@ -784,6 +789,7 @@ export function TraceWaterfall(props: TraceWaterfallProps) { traceEventView={props.traceEventView} /> <TracePreferencesDropdown + rootEventResults={props.rootEventResults} autogroup={ traceState.preferences.autogroup.parent && traceState.preferences.autogroup.sibling @@ -896,9 +902,7 @@ const GrabberContainer = styled('div')` `; const TraceToolbar = styled('div')` - flex-grow: 0; - display: grid; - grid-template-columns: 1fr min-content min-content min-content; + display: flex; gap: ${space(1)}; `;