123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- import {useEffect} from 'react';
- 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 {browserHistory} from 'sentry/utils/browserHistory';
- 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} 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',
- });
- useEffect(() => {
- const isMetricsData = getIsMetricsDataFromResults(queryData.data);
- mepContext.setIsMetricsData(isMetricsData);
- }, [mepContext, queryData.data]);
- const {data: tableData, isLoading, error} = queryData;
- 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};
- return (
- <SummaryContent
- location={location}
- organization={organization}
- eventView={eventView}
- projectId={projectId}
- transactionName={transactionName}
- isLoading={isLoading || isTotalCountQueryLoading}
- error={error || totalCountQueryError}
- totalValues={totals}
- onChangeFilter={onChangeFilter}
- spanOperationBreakdownFilter={spanOperationBreakdownFilter}
- />
- );
- }
- 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)));
|