123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635 |
- import {Fragment, useCallback, useMemo} from 'react';
- import styled from '@emotion/styled';
- import type {Location} from 'history';
- import omit from 'lodash/omit';
- import type {DropdownOption} from 'sentry/components/discover/transactionsList';
- import TransactionsList from 'sentry/components/discover/transactionsList';
- import SearchBar from 'sentry/components/events/searchBar';
- import * as Layout from 'sentry/components/layouts/thirds';
- import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
- import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
- import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
- import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
- import {TransactionSearchQueryBuilder} from 'sentry/components/performance/transactionSearchQueryBuilder';
- import {SuspectFunctionsTable} from 'sentry/components/profiling/suspectFunctions/suspectFunctionsTable';
- import type {ActionBarItem} from 'sentry/components/smartSearchBar';
- import {Tooltip} from 'sentry/components/tooltip';
- import {MAX_QUERY_LENGTH} from 'sentry/constants';
- import {IconWarning} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {Organization} from 'sentry/types/organization';
- import type {Project} from 'sentry/types/project';
- import {defined, generateQueryWithTag} from 'sentry/utils';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import {browserHistory} from 'sentry/utils/browserHistory';
- import type EventView from 'sentry/utils/discover/eventView';
- import {
- formatTagKey,
- isRelativeSpanOperationBreakdownField,
- SPAN_OP_BREAKDOWN_FIELDS,
- SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
- } from 'sentry/utils/discover/fields';
- import type {QueryError} from 'sentry/utils/discover/genericDiscoverQuery';
- import type {MetricsEnhancedPerformanceDataContext} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
- import {useMEPDataContext} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
- import {decodeScalar} from 'sentry/utils/queryString';
- import projectSupportsReplay from 'sentry/utils/replays/projectSupportsReplay';
- import {useRoutes} from 'sentry/utils/useRoutes';
- import withProjects from 'sentry/utils/withProjects';
- import type {Actions} from 'sentry/views/discover/table/cellAction';
- import {updateQuery} from 'sentry/views/discover/table/cellAction';
- import type {TableColumn} from 'sentry/views/discover/table/types';
- import Tags from 'sentry/views/discover/tags';
- import {canUseTransactionMetricsData} from 'sentry/views/performance/transactionSummary/transactionOverview/utils';
- import {
- PERCENTILE as VITAL_PERCENTILE,
- VITAL_GROUPS,
- } from 'sentry/views/performance/transactionSummary/transactionVitals/constants';
- import {isSummaryViewFrontend, isSummaryViewFrontendPageLoad} from '../../utils';
- import Filter, {
- decodeFilterFromLocation,
- filterToField,
- filterToSearchConditions,
- SpanOperationBreakdownFilter,
- } from '../filter';
- import {
- generateProfileLink,
- generateReplayLink,
- generateTraceLink,
- generateTransactionIdLink,
- normalizeSearchConditions,
- SidebarSpacer,
- TransactionFilterOptions,
- } from '../utils';
- import TransactionSummaryCharts from './charts';
- import {PerformanceAtScaleContextProvider} from './performanceAtScaleContext';
- import RelatedIssues from './relatedIssues';
- import SidebarCharts from './sidebarCharts';
- import StatusBreakdown from './statusBreakdown';
- import SuspectSpans from './suspectSpans';
- import {TagExplorer} from './tagExplorer';
- import UserStats from './userStats';
- type Props = {
- error: QueryError | null;
- eventView: EventView;
- isLoading: boolean;
- location: Location;
- onChangeFilter: (newFilter: SpanOperationBreakdownFilter) => void;
- organization: Organization;
- projectId: string;
- projects: Project[];
- spanOperationBreakdownFilter: SpanOperationBreakdownFilter;
- totalValues: Record<string, number> | null;
- transactionName: string;
- };
- function SummaryContent({
- eventView,
- location,
- totalValues,
- spanOperationBreakdownFilter,
- organization,
- projects,
- isLoading,
- error,
- projectId,
- transactionName,
- onChangeFilter,
- }: Props) {
- const routes = useRoutes();
- const mepDataContext = useMEPDataContext();
- const handleSearch = useCallback(
- (query: string) => {
- const queryParams = normalizeDateTimeParams({
- ...(location.query || {}),
- query,
- });
- // do not propagate pagination when making a new search
- const searchQueryParams = omit(queryParams, 'cursor');
- browserHistory.push({
- pathname: location.pathname,
- query: searchQueryParams,
- });
- },
- [location]
- );
- function generateTagUrl(key: string, value: string) {
- const query = generateQueryWithTag(location.query, {key: formatTagKey(key), value});
- return {
- ...location,
- query,
- };
- }
- function handleCellAction(column: TableColumn<React.ReactText>) {
- return (action: Actions, value: React.ReactText) => {
- const searchConditions = normalizeSearchConditions(eventView.query);
- updateQuery(searchConditions, action, column, value);
- browserHistory.push({
- pathname: location.pathname,
- query: {
- ...location.query,
- cursor: undefined,
- query: searchConditions.formatString(),
- },
- });
- };
- }
- function handleTransactionsListSortChange(value: string) {
- const target = {
- pathname: location.pathname,
- query: {...location.query, showTransactions: value, transactionCursor: undefined},
- };
- browserHistory.push(target);
- }
- function handleAllEventsViewClick() {
- trackAnalytics('performance_views.summary.view_in_transaction_events', {
- organization,
- });
- }
- function generateEventView(
- transactionsListEventView: EventView,
- transactionsListTitles: string[]
- ) {
- const {selected} = getTransactionsListSort(location, {
- p95: totalValues?.['p95()'] ?? 0,
- spanOperationBreakdownFilter,
- });
- const sortedEventView = transactionsListEventView.withSorts([selected.sort]);
- if (spanOperationBreakdownFilter === SpanOperationBreakdownFilter.NONE) {
- const fields = [
- // Remove the extra field columns
- ...sortedEventView.fields.slice(0, transactionsListTitles.length),
- ];
- // omit "Operation Duration" column
- sortedEventView.fields = fields.filter(({field}) => {
- return !isRelativeSpanOperationBreakdownField(field);
- });
- }
- return sortedEventView;
- }
- function generateActionBarItems(
- _org: Organization,
- _location: Location,
- _mepDataContext: MetricsEnhancedPerformanceDataContext
- ) {
- let items: ActionBarItem[] | undefined = undefined;
- if (!canUseTransactionMetricsData(_org, _mepDataContext)) {
- items = [
- {
- key: 'alert',
- makeAction: () => ({
- Button: () => <MetricsWarningIcon />,
- menuItem: {
- key: 'alert',
- },
- }),
- },
- ];
- }
- return items;
- }
- const trailingItems = useMemo(() => {
- if (!canUseTransactionMetricsData(organization, mepDataContext)) {
- return <MetricsWarningIcon />;
- }
- return null;
- }, [organization, mepDataContext]);
- const hasPerformanceChartInterpolation = organization.features.includes(
- 'performance-chart-interpolation'
- );
- const query = useMemo(() => {
- return decodeScalar(location.query.query, '');
- }, [location]);
- const totalCount = totalValues === null ? null : totalValues['count()'];
- // NOTE: This is not a robust check for whether or not a transaction is a front end
- // transaction, however it will suffice for now.
- const hasWebVitals =
- isSummaryViewFrontendPageLoad(eventView, projects) ||
- (totalValues !== null &&
- VITAL_GROUPS.some(group =>
- group.vitals.some(vital => {
- const functionName = `percentile(${vital},${VITAL_PERCENTILE})`;
- const field = functionName;
- return Number.isFinite(totalValues[field]) && totalValues[field] !== 0;
- })
- ));
- const isFrontendView = isSummaryViewFrontend(eventView, projects);
- const transactionsListTitles = [
- t('event id'),
- t('user'),
- t('total duration'),
- t('trace id'),
- t('timestamp'),
- ];
- const project = projects.find(p => p.id === projectId);
- let transactionsListEventView = eventView.clone();
- const fields = [...transactionsListEventView.fields];
- if (
- organization.features.includes('session-replay') &&
- project &&
- projectSupportsReplay(project)
- ) {
- transactionsListTitles.push(t('replay'));
- fields.push({field: 'replayId'});
- }
- if (
- // only show for projects that already sent a profile
- // once we have a more compact design we will show this for
- // projects that support profiling as well
- project?.hasProfiles &&
- (organization.features.includes('profiling') ||
- organization.features.includes('continuous-profiling'))
- ) {
- transactionsListTitles.push(t('profile'));
- if (organization.features.includes('profiling')) {
- fields.push({field: 'profile.id'});
- }
- if (organization.features.includes('continuous-profiling')) {
- fields.push({field: 'profiler.id'});
- fields.push({field: 'thread.id'});
- fields.push({field: 'precise.start_ts'});
- fields.push({field: 'precise.finish_ts'});
- }
- }
- // update search conditions
- const spanOperationBreakdownConditions = filterToSearchConditions(
- spanOperationBreakdownFilter,
- location
- );
- if (spanOperationBreakdownConditions) {
- eventView = eventView.clone();
- eventView.query = `${eventView.query} ${spanOperationBreakdownConditions}`.trim();
- transactionsListEventView = eventView.clone();
- }
- // update header titles of transactions list
- const operationDurationTableTitle =
- spanOperationBreakdownFilter === SpanOperationBreakdownFilter.NONE
- ? t('operation duration')
- : `${spanOperationBreakdownFilter} duration`;
- // add ops breakdown duration column as the 3rd column
- transactionsListTitles.splice(2, 0, operationDurationTableTitle);
- // span_ops_breakdown.relative is a preserved name and a marker for the associated
- // field renderer to be used to generate the relative ops breakdown
- let durationField = SPAN_OP_RELATIVE_BREAKDOWN_FIELD;
- if (spanOperationBreakdownFilter !== SpanOperationBreakdownFilter.NONE) {
- durationField = filterToField(spanOperationBreakdownFilter)!;
- }
- // add ops breakdown duration column as the 3rd column
- fields.splice(2, 0, {field: durationField});
- if (spanOperationBreakdownFilter === SpanOperationBreakdownFilter.NONE) {
- fields.push(
- ...SPAN_OP_BREAKDOWN_FIELDS.map(field => {
- return {field};
- })
- );
- }
- transactionsListEventView.fields = fields;
- const openAllEventsProps = {
- generatePerformanceTransactionEventsView: () => {
- const performanceTransactionEventsView = generateEventView(
- transactionsListEventView,
- transactionsListTitles
- );
- performanceTransactionEventsView.query = query;
- return performanceTransactionEventsView;
- },
- handleOpenAllEventsClick: handleAllEventsViewClick,
- };
- const hasNewSpansUIFlag =
- organization.features.includes('performance-spans-new-ui') &&
- organization.features.includes('insights-initial-modules');
- const projectIds = useMemo(() => eventView.project.slice(), [eventView.project]);
- function renderSearchBar() {
- if (organization.features.includes('search-query-builder-performance')) {
- return (
- <TransactionSearchQueryBuilder
- projects={projectIds}
- initialQuery={query}
- onSearch={handleSearch}
- searchSource="transaction_summary"
- disableLoadingTags // already loaded by the parent component
- filterKeyMenuWidth={420}
- trailingItems={trailingItems}
- />
- );
- }
- return (
- <SearchBar
- searchSource="transaction_summary"
- organization={organization}
- projectIds={eventView.project}
- query={query}
- fields={eventView.fields}
- onSearch={handleSearch}
- maxQueryLength={MAX_QUERY_LENGTH}
- actionBarItems={generateActionBarItems(organization, location, mepDataContext)}
- />
- );
- }
- return (
- <Fragment>
- <Layout.Main>
- <FilterActions>
- <Filter
- organization={organization}
- currentFilter={spanOperationBreakdownFilter}
- onChangeFilter={onChangeFilter}
- />
- <PageFilterBar condensed>
- <EnvironmentPageFilter />
- <DatePageFilter />
- </PageFilterBar>
- <StyledSearchBarWrapper>{renderSearchBar()}</StyledSearchBarWrapper>
- </FilterActions>
- <PerformanceAtScaleContextProvider>
- <TransactionSummaryCharts
- organization={organization}
- location={location}
- eventView={eventView}
- totalValue={totalCount}
- currentFilter={spanOperationBreakdownFilter}
- withoutZerofill={hasPerformanceChartInterpolation}
- project={project}
- />
- <TransactionsList
- location={location}
- organization={organization}
- eventView={transactionsListEventView}
- {...openAllEventsProps}
- showTransactions={
- decodeScalar(
- location.query.showTransactions,
- TransactionFilterOptions.SLOW
- ) as TransactionFilterOptions
- }
- breakdown={decodeFilterFromLocation(location)}
- titles={transactionsListTitles}
- handleDropdownChange={handleTransactionsListSortChange}
- generateLink={{
- id: generateTransactionIdLink(transactionName),
- trace: generateTraceLink(eventView.normalizeDateSelection(location)),
- replayId: generateReplayLink(routes),
- 'profile.id': generateProfileLink(),
- }}
- handleCellAction={handleCellAction}
- {...getTransactionsListSort(location, {
- p95: totalValues?.['p95()'] ?? 0,
- spanOperationBreakdownFilter,
- })}
- forceLoading={isLoading}
- referrer="performance.transactions_summary"
- supportsInvestigationRule
- />
- </PerformanceAtScaleContextProvider>
- {!hasNewSpansUIFlag && (
- <SuspectSpans
- location={location}
- organization={organization}
- eventView={eventView}
- totals={
- defined(totalValues?.['count()'])
- ? {'count()': totalValues!['count()']}
- : null
- }
- projectId={projectId}
- transactionName={transactionName}
- />
- )}
- <TagExplorer
- eventView={eventView}
- organization={organization}
- location={location}
- projects={projects}
- transactionName={transactionName}
- currentFilter={spanOperationBreakdownFilter}
- />
- <SuspectFunctionsTable
- project={project}
- transaction={transactionName}
- analyticsPageSource="performance_transaction"
- />
- <RelatedIssues
- organization={organization}
- location={location}
- transaction={transactionName}
- start={eventView.start}
- end={eventView.end}
- statsPeriod={eventView.statsPeriod}
- />
- </Layout.Main>
- <Layout.Side>
- <UserStats
- organization={organization}
- location={location}
- isLoading={isLoading}
- hasWebVitals={hasWebVitals}
- error={error}
- totals={totalValues}
- transactionName={transactionName}
- eventView={eventView}
- />
- {!isFrontendView && (
- <StatusBreakdown
- eventView={eventView}
- organization={organization}
- location={location}
- />
- )}
- <SidebarSpacer />
- <SidebarCharts
- organization={organization}
- isLoading={isLoading}
- error={error}
- totals={totalValues}
- eventView={eventView}
- transactionName={transactionName}
- />
- <SidebarSpacer />
- <Tags
- generateUrl={generateTagUrl}
- totalValues={totalCount}
- eventView={eventView}
- organization={organization}
- location={location}
- />
- </Layout.Side>
- </Fragment>
- );
- }
- function getFilterOptions({
- p95,
- spanOperationBreakdownFilter,
- }: {
- p95: number;
- spanOperationBreakdownFilter: SpanOperationBreakdownFilter;
- }): DropdownOption[] {
- if (spanOperationBreakdownFilter === SpanOperationBreakdownFilter.NONE) {
- return [
- {
- sort: {kind: 'asc', field: 'transaction.duration'},
- value: TransactionFilterOptions.FASTEST,
- label: t('Fastest Transactions'),
- },
- {
- query: p95 > 0 ? [['transaction.duration', `<=${p95.toFixed(0)}`]] : undefined,
- sort: {kind: 'desc', field: 'transaction.duration'},
- value: TransactionFilterOptions.SLOW,
- label: t('Slow Transactions (p95)'),
- },
- {
- sort: {kind: 'desc', field: 'transaction.duration'},
- value: TransactionFilterOptions.OUTLIER,
- label: t('Outlier Transactions (p100)'),
- },
- {
- sort: {kind: 'desc', field: 'timestamp'},
- value: TransactionFilterOptions.RECENT,
- label: t('Recent Transactions'),
- },
- ];
- }
- const field = filterToField(spanOperationBreakdownFilter)!;
- const operationName = spanOperationBreakdownFilter;
- return [
- {
- sort: {kind: 'asc', field},
- value: TransactionFilterOptions.FASTEST,
- label: t('Fastest %s Operations', operationName),
- },
- {
- query: p95 > 0 ? [['transaction.duration', `<=${p95.toFixed(0)}`]] : undefined,
- sort: {kind: 'desc', field},
- value: TransactionFilterOptions.SLOW,
- label: t('Slow %s Operations (p95)', operationName),
- },
- {
- sort: {kind: 'desc', field},
- value: TransactionFilterOptions.OUTLIER,
- label: t('Outlier %s Operations (p100)', operationName),
- },
- {
- sort: {kind: 'desc', field: 'timestamp'},
- value: TransactionFilterOptions.RECENT,
- label: t('Recent Transactions'),
- },
- ];
- }
- function getTransactionsListSort(
- location: Location,
- options: {p95: number; spanOperationBreakdownFilter: SpanOperationBreakdownFilter}
- ): {options: DropdownOption[]; selected: DropdownOption} {
- const sortOptions = getFilterOptions(options);
- const urlParam = decodeScalar(
- location.query.showTransactions,
- TransactionFilterOptions.SLOW
- );
- const selectedSort = sortOptions.find(opt => opt.value === urlParam) || sortOptions[0];
- return {selected: selectedSort, options: sortOptions};
- }
- function MetricsWarningIcon() {
- return (
- <Tooltip
- title={t(
- 'Based on your search criteria and sample rate, the events available may be limited.'
- )}
- >
- <StyledIconWarning
- data-test-id="search-metrics-fallback-warning"
- size="sm"
- color="warningText"
- />
- </Tooltip>
- );
- }
- const FilterActions = styled('div')`
- display: grid;
- gap: ${space(2)};
- margin-bottom: ${space(2)};
- @media (min-width: ${p => p.theme.breakpoints.small}) {
- grid-template-columns: repeat(2, min-content);
- }
- @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
- grid-template-columns: auto auto 1fr;
- }
- `;
- const StyledSearchBarWrapper = styled('div')`
- @media (min-width: ${p => p.theme.breakpoints.small}) {
- order: 1;
- grid-column: 1/4;
- }
- @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
- order: initial;
- grid-column: auto;
- }
- `;
- const StyledIconWarning = styled(IconWarning)`
- display: block;
- `;
- export default withProjects(SummaryContent);
|