mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
345 lines
10 KiB
TypeScript
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)));
|