123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- import {Fragment, useRef, useState} from 'react';
- import {useTheme} from '@emotion/react';
- import styled from '@emotion/styled';
- import {Location} from 'history';
- import moment from 'moment';
- import {getInterval} from 'sentry/components/charts/utils';
- import {CompactSelect} from 'sentry/components/compactSelect';
- import DatePageFilter from 'sentry/components/datePageFilter';
- import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import {ReactEchartsRef, Series} from 'sentry/types/echarts';
- import {useApiQuery} from 'sentry/utils/queryClient';
- import useOrganization from 'sentry/utils/useOrganization';
- import usePageFilters from 'sentry/utils/usePageFilters';
- import Chart, {useSynchronizeCharts} from 'sentry/views/starfish/components/chart';
- import ChartPanel from 'sentry/views/starfish/components/chartPanel';
- import {INTERNAL_API_REGEX} from 'sentry/views/starfish/modules/APIModule/constants';
- import {HostDetails} from 'sentry/views/starfish/modules/APIModule/hostDetails';
- import {queryToSeries} from 'sentry/views/starfish/modules/databaseModule/utils';
- import {PERIOD_REGEX} from 'sentry/views/starfish/utils/dates';
- import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery';
- import {zeroFillSeries} from 'sentry/views/starfish/utils/zeroFillSeries';
- import {EndpointDataRow} from 'sentry/views/starfish/views/endpointDetails';
- import EndpointTable from './endpointTable';
- import HostTable from './hostTable';
- import {
- getEndpointDomainsEventView,
- getEndpointDomainsQuery,
- getEndpointGraphEventView,
- getEndpointGraphQuery,
- useGetTransactionsForHosts,
- } from './queries';
- const HTTP_ACTION_OPTIONS = [
- {value: '', label: 'All'},
- ...['GET', 'POST', 'PUT', 'DELETE'].map(action => ({
- value: action,
- label: action,
- })),
- ];
- type Props = {
- location: Location;
- onSelect: (row: EndpointDataRow) => void;
- };
- export type DataRow = {
- count: number;
- description: string;
- domain: string;
- };
- export default function APIModuleView({location, onSelect}: Props) {
- const themes = useTheme();
- const pageFilter = usePageFilters();
- const [state, setState] = useState<{
- action: string;
- domain: string;
- transaction: string;
- }>({
- action: '',
- domain: '',
- transaction: '',
- });
- const endpointTableRef = useRef<HTMLInputElement>(null);
- const organization = useOrganization();
- const endpointsDomainEventView = getEndpointDomainsEventView({
- datetime: pageFilter.selection.datetime,
- });
- const endpointsDomainQuery = getEndpointDomainsQuery({
- datetime: pageFilter.selection.datetime,
- });
- const {selection} = pageFilter;
- const {projects, environments, datetime} = selection;
- useApiQuery<null>(
- [
- `/organizations/${organization.slug}/events-starfish/`,
- {
- query: {
- ...{
- environment: environments,
- project: projects.map(proj => String(proj)),
- },
- ...normalizeDateTimeParams(datetime),
- },
- },
- ],
- {
- staleTime: 10,
- }
- );
- const {isLoading: _isDomainsLoading, data: domains} = useSpansQuery({
- eventView: endpointsDomainEventView,
- queryString: endpointsDomainQuery,
- initialData: [],
- });
- const endpointsGraphEventView = getEndpointGraphEventView({
- datetime: pageFilter.selection.datetime,
- });
- const {isLoading: isGraphLoading, data: graphData} = useSpansQuery({
- eventView: endpointsGraphEventView,
- queryString: getEndpointGraphQuery({
- datetime: pageFilter.selection.datetime,
- }),
- initialData: [],
- });
- const quantiles = [
- 'p50(span.self_time)',
- 'p75(span.self_time)',
- 'p95(span.self_time)',
- 'p99(span.self_time)',
- ];
- const seriesByQuantile: {[quantile: string]: Series} = {};
- quantiles.forEach(quantile => {
- seriesByQuantile[quantile] = {
- seriesName: quantile,
- data: [],
- };
- });
- const countSeries: Series = {
- seriesName: 'count',
- data: [],
- };
- const failureRateSeries: Series = {
- seriesName: 'failure_rate',
- data: [],
- };
- graphData.forEach(datum => {
- quantiles.forEach(quantile => {
- seriesByQuantile[quantile].data.push({
- value: datum[quantile],
- name: datum.interval,
- });
- });
- countSeries.data.push({
- value: datum['count()'],
- name: datum.interval,
- });
- failureRateSeries.data.push({
- value: datum['failure_rate()'],
- name: datum.interval,
- });
- });
- const [_, num, unit] = pageFilter.selection.datetime.period?.match(PERIOD_REGEX) ?? [];
- const startTime =
- num && unit
- ? moment().subtract(num, unit as 'h' | 'd')
- : moment(pageFilter.selection.datetime.start);
- const endTime = moment(pageFilter.selection.datetime.end ?? undefined);
- const [zeroFilledQuantiles, zeroFilledCounts, zeroFilledFailureRate] = [
- seriesByQuantile,
- [countSeries],
- [failureRateSeries],
- ].map(seriesGroup =>
- Object.values(seriesGroup).map(series =>
- zeroFillSeries(series, moment.duration(12, 'hours'), startTime, endTime)
- )
- );
- const setAction = (action: string) => {
- setState({
- ...state,
- action,
- });
- };
- const setDomain = (domain: string) => {
- setState({
- ...state,
- domain,
- });
- };
- const domainOptions = [
- {value: '', label: 'All'},
- ...domains
- .filter(({domain}) => domain !== '')
- .map(({domain}) => ({
- value: domain,
- label: domain,
- })),
- ];
- const interval = getInterval(pageFilter.selection.datetime, 'low');
- const {isLoading: isTopTransactionDataLoading, data: topTransactionsData} =
- useGetTransactionsForHosts(
- domains
- .map(({domain}) => domain)
- .filter(domain => !domain.match(INTERNAL_API_REGEX)),
- interval
- );
- const tpmTransactionSeries = queryToSeries(topTransactionsData, 'group', 'epm()');
- const p75TransactionSeries = queryToSeries(
- topTransactionsData,
- 'group',
- 'p75(transaction.duration)'
- );
- const loading = isGraphLoading || isTopTransactionDataLoading;
- useSynchronizeCharts([!loading]);
- return (
- <Fragment>
- <FilterOptionsContainer>
- <CompactSelect
- triggerProps={{prefix: t('Service')}}
- value="project"
- options={[{value: 'project', label: 'Project'}]}
- onChange={() => void 0}
- />
- <DatePageFilter alignDropdown="left" />
- </FilterOptionsContainer>
- <ChartsContainer>
- <ChartsContainerItem>
- <ChartPanel title={t('Top Transactions Throughput')}>
- <APIModuleChart data={tpmTransactionSeries} loading={loading} />
- </ChartPanel>
- </ChartsContainerItem>
- <ChartsContainerItem>
- <ChartPanel title={t('Top Transactions p75')}>
- <APIModuleChart data={p75TransactionSeries} loading={loading} />
- </ChartPanel>
- </ChartsContainerItem>
- </ChartsContainer>
- <ChartsContainer>
- <ChartsContainerItem>
- <ChartPanel title={t('Throughput')}>
- <APIModuleChart data={zeroFilledCounts} loading={loading} />
- </ChartPanel>
- </ChartsContainerItem>
- <ChartsContainerItem>
- <ChartPanel title={t('Response Time')}>
- <APIModuleChart data={zeroFilledQuantiles} loading={loading} />
- </ChartPanel>
- </ChartsContainerItem>
- <ChartsContainerItem>
- <ChartPanel title={t('Error Rate')}>
- <APIModuleChart
- data={zeroFilledFailureRate}
- loading={loading}
- chartColors={[themes.charts.getColorPalette(2)[2]]}
- />
- </ChartPanel>
- </ChartsContainerItem>
- </ChartsContainer>
- <HostTable
- location={location}
- setDomainFilter={domain => {
- setDomain(domain);
- // TODO: Cheap way to scroll to the endpoints table without waiting for async request
- setTimeout(() => {
- endpointTableRef.current?.scrollIntoView({
- behavior: 'smooth',
- inline: 'start',
- });
- }, 200);
- }}
- />
- <FilterOptionsContainer>
- <CompactSelect
- triggerProps={{prefix: t('Operation')}}
- value={state.action}
- options={HTTP_ACTION_OPTIONS}
- onChange={({value}) => setAction(value)}
- />
- <CompactSelect
- triggerProps={{prefix: t('Domain')}}
- value={state.domain}
- options={domainOptions}
- onChange={({value}) => setDomain(value)}
- />
- </FilterOptionsContainer>
- <div ref={endpointTableRef}>
- {state.domain && <HostDetails host={state.domain} />}
- <EndpointTable
- location={location}
- onSelect={onSelect}
- filterOptions={{...state, datetime: pageFilter.selection.datetime}}
- />
- </div>
- </Fragment>
- );
- }
- function APIModuleChart({
- data,
- loading,
- chartColors,
- forwardedRef,
- chartGroup,
- }: {
- data: Series[];
- loading: boolean;
- chartColors?: string[];
- chartGroup?: string;
- forwardedRef?: React.RefObject<ReactEchartsRef>;
- }) {
- return (
- <Chart
- statsPeriod="24h"
- height={140}
- data={data}
- start=""
- end=""
- loading={loading}
- utc={false}
- grid={{
- left: '0',
- right: '0',
- top: '8px',
- bottom: '0',
- }}
- definedAxisTicks={4}
- stacked
- isLineChart
- chartColors={chartColors}
- disableXAxis
- forwardedRef={forwardedRef}
- chartGroup={chartGroup}
- />
- );
- }
- const ChartsContainer = styled('div')`
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- gap: ${space(2)};
- `;
- const ChartsContainerItem = styled('div')`
- flex: 1;
- `;
- const FilterOptionsContainer = styled('div')`
- display: flex;
- flex-direction: row;
- gap: ${space(1)};
- margin-bottom: ${space(2)};
- `;
|