Files
2024-02-15 17:01:50 +00:00

345 lines
10 KiB
TypeScript

import {useEffect} from 'react';
import {browserHistory} from 'react-router';
import type {Location} from 'history';
import {loadOrganizationTags} from 'sentry/actionCreators/tags';
import LoadingContainer from 'sentry/components/loading/loadingContainer';
import {t} from 'sentry/locale';
import type {Organization, PageFilters, Project} from 'sentry/types';
import {trackAnalytics} from 'sentry/utils/analytics';
import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
import EventView from 'sentry/utils/discover/eventView';
import type {Column, QueryFieldValue} from 'sentry/utils/discover/fields';
import {isAggregateField} from 'sentry/utils/discover/fields';
import type {WebVital} from 'sentry/utils/fields';
import {useMetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality';
import {
getIsMetricsDataFromResults,
useMEPDataContext,
} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
import {
MEPSettingProvider,
useMEPSettingContext,
} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
import {removeHistogramQueryStrings} from 'sentry/utils/performance/histogram';
import {decodeScalar} from 'sentry/utils/queryString';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import useApi from 'sentry/utils/useApi';
import withOrganization from 'sentry/utils/withOrganization';
import withPageFilters from 'sentry/utils/withPageFilters';
import withProjects from 'sentry/utils/withProjects';
import {
getTransactionMEPParamsIfApplicable,
getUnfilteredTotalsEventView,
} from 'sentry/views/performance/transactionSummary/transactionOverview/utils';
import {addRoutePerformanceContext} from '../../utils';
import {
decodeFilterFromLocation,
filterToLocationQuery,
SpanOperationBreakdownFilter,
} from '../filter';
import type {ChildProps} from '../pageLayout';
import PageLayout from '../pageLayout';
import Tab from '../tabs';
import {
PERCENTILE as VITAL_PERCENTILE,
VITAL_GROUPS,
} from '../transactionVitals/constants';
import {ZOOM_END, ZOOM_START} from './latencyChart/utils';
import SummaryContent from './content';
// Used to cast the totals request to numbers
// as React.ReactText
type TotalValues = Record<string, number>;
type Props = {
location: Location;
organization: Organization;
projects: Project[];
selection: PageFilters;
};
function TransactionOverview(props: Props) {
const api = useApi();
const {location, selection, organization, projects} = props;
useEffect(() => {
loadOrganizationTags(api, organization.slug, selection);
addRoutePerformanceContext(selection);
trackAnalytics('performance_views.transaction_summary.view', {
organization,
});
}, [selection, organization, api]);
return (
<MEPSettingProvider>
<PageLayout
location={location}
organization={organization}
projects={projects}
tab={Tab.TRANSACTION_SUMMARY}
getDocumentTitle={getDocumentTitle}
generateEventView={generateEventView}
childComponent={CardinalityLoadingWrapper}
/>
</MEPSettingProvider>
);
}
function CardinalityLoadingWrapper(props: ChildProps) {
const mepCardinalityContext = useMetricsCardinalityContext();
if (mepCardinalityContext.isLoading) {
return <LoadingContainer isLoading />;
}
return <OverviewContentWrapper {...props} />;
}
function OverviewContentWrapper(props: ChildProps) {
const {
location,
organization,
eventView,
projectId,
transactionName,
transactionThreshold,
transactionThresholdMetric,
} = props;
const mepContext = useMEPDataContext();
const mepSetting = useMEPSettingContext();
const mepCardinalityContext = useMetricsCardinalityContext();
const queryExtras = getTransactionMEPParamsIfApplicable(
mepSetting,
mepCardinalityContext,
organization
);
const queryData = useDiscoverQuery({
eventView: getTotalsEventView(organization, eventView),
orgSlug: organization.slug,
location,
transactionThreshold,
transactionThresholdMetric,
referrer: 'api.performance.transaction-summary',
queryExtras,
options: {
refetchOnWindowFocus: false,
},
});
// Count has to be total indexed events count because it's only used
// in indexed events contexts
const totalCountQueryData = useDiscoverQuery({
eventView: getTotalCountEventView(organization, eventView),
orgSlug: organization.slug,
location,
transactionThreshold,
transactionThresholdMetric,
referrer: 'api.performance.transaction-summary',
});
// Unfiltered count has to be total indexed events count because it's only used
// in indexed events contexts
const additionalQueryData = useDiscoverQuery({
eventView: getUnfilteredTotalsEventView(eventView, location, ['count']),
orgSlug: organization.slug,
location,
transactionThreshold,
transactionThresholdMetric,
referrer: 'api.performance.transaction-summary',
});
useEffect(() => {
const isMetricsData = getIsMetricsDataFromResults(queryData.data);
mepContext.setIsMetricsData(isMetricsData);
}, [mepContext, queryData.data]);
const {data: tableData, isLoading, error} = queryData;
const {
data: unfilteredTableData,
isLoading: isAdditionalQueryLoading,
error: additionalQueryError,
} = additionalQueryData;
const {
data: totalCountTableData,
isLoading: isTotalCountQueryLoading,
error: totalCountQueryError,
} = totalCountQueryData;
const spanOperationBreakdownFilter = decodeFilterFromLocation(location);
const onChangeFilter = (newFilter: SpanOperationBreakdownFilter) => {
trackAnalytics('performance_views.filter_dropdown.selection', {
organization,
action: newFilter as string,
});
const nextQuery: Location['query'] = {
...removeHistogramQueryStrings(location, [ZOOM_START, ZOOM_END]),
...filterToLocationQuery(newFilter),
};
if (newFilter === SpanOperationBreakdownFilter.NONE) {
delete nextQuery.breakdown;
}
browserHistory.push({
pathname: location.pathname,
query: nextQuery,
});
};
let totals: TotalValues | null =
(tableData?.data?.[0] as {
[k: string]: number;
}) ?? null;
const totalCountData: TotalValues | null =
(totalCountTableData?.data?.[0] as {[k: string]: number}) ?? null;
// Count is always a count of indexed events,
// while other fields could be either metrics or index based
totals = {...totals, ...totalCountData};
const unfilteredTotals: TotalValues | null =
(unfilteredTableData?.data?.[0] as {[k: string]: number}) ?? null;
return (
<SummaryContent
location={location}
organization={organization}
eventView={eventView}
projectId={projectId}
transactionName={transactionName}
isLoading={isLoading || isAdditionalQueryLoading || isTotalCountQueryLoading}
error={error || additionalQueryError || totalCountQueryError}
totalValues={totals}
onChangeFilter={onChangeFilter}
spanOperationBreakdownFilter={spanOperationBreakdownFilter}
unfilteredTotalValues={unfilteredTotals}
/>
);
}
function getDocumentTitle(transactionName: string): string {
const hasTransactionName =
typeof transactionName === 'string' && String(transactionName).trim().length > 0;
if (hasTransactionName) {
return [String(transactionName).trim(), t('Performance')].join(' - ');
}
return [t('Summary'), t('Performance')].join(' - ');
}
function generateEventView({
location,
transactionName,
}: {
location: Location;
organization: Organization;
transactionName: string;
}): EventView {
// Use the user supplied query but overwrite any transaction or event type
// conditions they applied.
const query = decodeScalar(location.query.query, '');
const conditions = new MutableSearch(query);
conditions.setFilterValues('event.type', ['transaction']);
conditions.setFilterValues('transaction', [transactionName]);
Object.keys(conditions.filters).forEach(field => {
if (isAggregateField(field)) {
conditions.removeFilter(field);
}
});
const fields = ['id', 'user.display', 'transaction.duration', 'trace', 'timestamp'];
return EventView.fromNewQueryWithLocation(
{
id: undefined,
version: 2,
name: transactionName,
fields,
query: conditions.formatString(),
projects: [],
},
location
);
}
function getTotalCountEventView(
_organization: Organization,
eventView: EventView
): EventView {
const totalCountField: QueryFieldValue = {
kind: 'function',
function: ['count', '', undefined, undefined],
};
return eventView.withColumns([totalCountField]);
}
function getTotalsEventView(
_organization: Organization,
eventView: EventView
): EventView {
const vitals = VITAL_GROUPS.map(({vitals: vs}) => vs).reduce((keys: WebVital[], vs) => {
vs.forEach(vital => keys.push(vital));
return keys;
}, []);
const totalsColumns: QueryFieldValue[] = [
{
kind: 'function',
function: ['p95', '', undefined, undefined],
},
{
kind: 'function',
function: ['count_unique', 'user', undefined, undefined],
},
{
kind: 'function',
function: ['failure_rate', '', undefined, undefined],
},
{
kind: 'function',
function: ['tpm', '', undefined, undefined],
},
{
kind: 'function',
function: ['count_miserable', 'user', undefined, undefined],
},
{
kind: 'function',
function: ['user_misery', '', undefined, undefined],
},
{
kind: 'function',
function: ['apdex', '', undefined, undefined],
},
{
kind: 'function',
function: ['sum', 'transaction.duration', undefined, undefined],
},
];
return eventView.withColumns([
...totalsColumns,
...vitals.map(
vital =>
({
kind: 'function',
function: ['percentile', vital, VITAL_PERCENTILE.toString(), undefined],
}) as Column
),
]);
}
export default withPageFilters(withProjects(withOrganization(TransactionOverview)));