Transaction type handling and breakdown chart (#83587)

This commit is contained in:
Nathan L Smith 2020-11-24 06:37:24 -06:00 committed by GitHub
parent 7d8ca10fbc
commit 3714658760
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 213 additions and 87 deletions

View file

@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
getFirstTransactionType,
isJavaAgentName,
isRumAgentName,
} from './agent_name';
describe('agent name helpers', () => {
describe('getFirstTransactionType', () => {
describe('with no transaction types', () => {
expect(getFirstTransactionType([])).toBeUndefined();
});
describe('with a non-rum agent', () => {
it('returns "request"', () => {
expect(getFirstTransactionType(['worker', 'request'], 'java')).toEqual(
'request'
);
});
describe('with no request types', () => {
it('returns the first type', () => {
expect(
getFirstTransactionType(['worker', 'shirker'], 'java')
).toEqual('worker');
});
});
});
describe('with a rum agent', () => {
it('returns "page-load"', () => {
expect(
getFirstTransactionType(['http-request', 'page-load'], 'js-base')
).toEqual('page-load');
});
});
});
describe('isJavaAgentName', () => {
describe('when the agent name is java', () => {
it('returns true', () => {
expect(isJavaAgentName('java')).toEqual(true);
});
});
describe('when the agent name is not java', () => {
it('returns true', () => {
expect(isJavaAgentName('not java')).toEqual(false);
});
});
});
describe('isRumAgentName', () => {
describe('when the agent name is js-base', () => {
it('returns true', () => {
expect(isRumAgentName('js-base')).toEqual(true);
});
});
describe('when the agent name is rum-js', () => {
it('returns true', () => {
expect(isRumAgentName('rum-js')).toEqual(true);
});
});
describe('when the agent name is opentelemetry/webjs', () => {
it('returns true', () => {
expect(isRumAgentName('opentelemetry/webjs')).toEqual(true);
});
});
describe('when the agent name something else', () => {
it('returns true', () => {
expect(isRumAgentName('java')).toEqual(false);
});
});
});
});

View file

@ -5,6 +5,10 @@
*/
import { AgentName } from '../typings/es_schemas/ui/fields/agent';
import {
TRANSACTION_PAGE_LOAD,
TRANSACTION_REQUEST,
} from './transaction_types';
/*
* Agent names can be any string. This list only defines the official agents
@ -46,10 +50,24 @@ export const RUM_AGENT_NAMES: AgentName[] = [
'opentelemetry/webjs',
];
export function isRumAgentName(
function getDefaultTransactionTypeForAgentName(agentName?: string) {
return isRumAgentName(agentName)
? TRANSACTION_PAGE_LOAD
: TRANSACTION_REQUEST;
}
export function getFirstTransactionType(
transactionTypes: string[],
agentName?: string
): agentName is 'js-base' | 'rum-js' | 'opentelemetry/webjs' {
return RUM_AGENT_NAMES.includes(agentName! as AgentName);
) {
const defaultTransactionType = getDefaultTransactionTypeForAgentName(
agentName
);
return (
transactionTypes.find((type) => type === defaultTransactionType) ??
transactionTypes[0]
);
}
export function isJavaAgentName(
@ -57,3 +75,9 @@ export function isJavaAgentName(
): agentName is 'java' {
return agentName === 'java';
}
export function isRumAgentName(
agentName?: string
): agentName is 'js-base' | 'rum-js' | 'opentelemetry/webjs' {
return RUM_AGENT_NAMES.includes(agentName! as AgentName);
}

View file

@ -14,14 +14,9 @@ const { createJestConfig } = require('../../dev-tools/jest/create_jest_config');
const { resolve } = require('path');
const rootDir = resolve(__dirname, '.');
const xPackKibanaDirectory = resolve(__dirname, '../..');
const kibanaDirectory = resolve(__dirname, '../../..');
const jestConfig = createJestConfig({
kibanaDirectory,
rootDir,
xPackKibanaDirectory,
});
const jestConfig = createJestConfig({ kibanaDirectory, rootDir });
module.exports = {
...jestConfig,

View file

@ -23,7 +23,7 @@ import { ServiceMap } from '../ServiceMap';
import { ServiceMetrics } from '../service_metrics';
import { ServiceNodeOverview } from '../ServiceNodeOverview';
import { ServiceOverview } from '../service_overview';
import { TransactionOverview } from '../TransactionOverview';
import { TransactionOverview } from '../transaction_overview';
interface Tab {
key: string;

View file

@ -16,6 +16,7 @@ import React from 'react';
import { useTrackPageview } from '../../../../../observability/public';
import { isRumAgentName } from '../../../../common/agent_name';
import { ChartsSyncContextProvider } from '../../../context/charts_sync_context';
import { TransactionBreakdownChart } from '../../shared/charts/transaction_breakdown_chart';
import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart';
import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink';
import { SearchBar } from '../../shared/search_bar';
@ -103,22 +104,7 @@ export function ServiceOverview({
<EuiFlexItem>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={4}>
<EuiPanel>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serviceOverview.averageDurationBySpanTypeChartTitle',
{
defaultMessage: 'Average duration by span type',
}
)}
</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
<TransactionBreakdownChart showAnnotations={false} />
</EuiFlexItem>
<EuiFlexItem grow={6}>
<EuiPanel>

View file

@ -17,6 +17,8 @@ import { MockUrlParamsContextProvider } from '../../../context/UrlParamsContext/
import * as useDynamicIndexPatternHooks from '../../../hooks/useDynamicIndexPattern';
import * as useFetcherHooks from '../../../hooks/useFetcher';
import { FETCH_STATUS } from '../../../hooks/useFetcher';
import * as useAnnotationsHooks from '../../../hooks/use_annotations';
import * as useTransactionBreakdownHooks from '../../../hooks/use_transaction_breakdown';
import { renderWithTheme } from '../../../utils/testHelpers';
import { ServiceOverview } from './';
@ -53,6 +55,9 @@ function Wrapper({ children }: { children?: ReactNode }) {
describe('ServiceOverview', () => {
it('renders', () => {
jest
.spyOn(useAnnotationsHooks, 'useAnnotations')
.mockReturnValue({ annotations: [] });
jest
.spyOn(useDynamicIndexPatternHooks, 'useDynamicIndexPattern')
.mockReturnValue({
@ -71,6 +76,13 @@ describe('ServiceOverview', () => {
refetch: () => {},
status: FETCH_STATUS.SUCCESS,
});
jest
.spyOn(useTransactionBreakdownHooks, 'useTransactionBreakdown')
.mockReturnValue({
data: { timeseries: [] },
error: undefined,
status: FETCH_STATUS.SUCCESS,
});
expect(() =>
renderWithTheme(<ServiceOverview serviceName="test service name" />, {

View file

@ -18,7 +18,6 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { Location } from 'history';
import { first } from 'lodash';
import React, { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { useTrackPageview } from '../../../../../observability/public';
@ -29,6 +28,7 @@ import { useServiceTransactionTypes } from '../../../hooks/useServiceTransaction
import { useTransactionCharts } from '../../../hooks/useTransactionCharts';
import { useTransactionList } from '../../../hooks/useTransactionList';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { useTransactionType } from '../../../hooks/use_transaction_type';
import { TransactionCharts } from '../../shared/charts/transaction_charts';
import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
import { fromQuery, toQuery } from '../../shared/Links/url_helpers';
@ -41,23 +41,22 @@ import { useRedirect } from './useRedirect';
import { UserExperienceCallout } from './user_experience_callout';
function getRedirectLocation({
urlParams,
location,
serviceTransactionTypes,
transactionType,
urlParams,
}: {
location: Location;
transactionType?: string;
urlParams: IUrlParams;
serviceTransactionTypes: string[];
}): Location | undefined {
const { transactionType } = urlParams;
const firstTransactionType = first(serviceTransactionTypes);
const transactionTypeFromUrlParams = urlParams.transactionType;
if (!transactionType && firstTransactionType) {
if (!transactionTypeFromUrlParams && transactionType) {
return {
...location,
search: fromQuery({
...toQuery(location.search),
transactionType: firstTransactionType,
transactionType,
}),
};
}
@ -70,19 +69,11 @@ interface TransactionOverviewProps {
export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
const location = useLocation();
const { urlParams } = useUrlParams();
const { transactionType } = urlParams;
// TODO: fetching of transaction types should perhaps be lifted since it is needed in several places. Context?
const transactionType = useTransactionType();
const serviceTransactionTypes = useServiceTransactionTypes(urlParams);
// redirect to first transaction type
useRedirect(
getRedirectLocation({
urlParams,
location,
serviceTransactionTypes,
})
);
useRedirect(getRedirectLocation({ location, transactionType, urlParams }));
const {
data: transactionCharts,

View file

@ -46,17 +46,7 @@ export function SparkPlot(props: Props) {
return (
<Chart size={{ height: px(24), width }}>
<Settings
theme={{
...chartTheme,
background: {
...chartTheme.background,
color: 'transparent',
},
}}
showLegend={false}
tooltip="none"
/>
<Settings theme={chartTheme} showLegend={false} tooltip="none" />
<AreaSeries
id="area"
xScaleType={ScaleType.Time}

View file

@ -16,11 +16,11 @@ import {
Position,
ScaleType,
Settings,
SettingsSpec,
} from '@elastic/charts';
import moment from 'moment';
import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { useChartTheme } from '../../../../../observability/public';
import { TimeSeries } from '../../../../typings/timeseries';
import { FETCH_STATUS } from '../../../hooks/useFetcher';
import { useUrlParams } from '../../../hooks/useUrlParams';
@ -59,6 +59,7 @@ export function TimeseriesChart({
}: Props) {
const history = useHistory();
const chartRef = React.createRef<Chart>();
const chartTheme = useChartTheme();
const { event, setEvent } = useChartsSync();
const { urlParams } = useUrlParams();
const { start, end } = urlParams;
@ -74,13 +75,6 @@ export function TimeseriesChart({
const xFormatter = niceTimeFormatter([min, max]);
const chartTheme: SettingsSpec['theme'] = {
lineSeriesStyle: {
point: { visible: false },
line: { strokeWidth: 2 },
},
};
const isEmpty = timeseries
.map((serie) => serie.data)
.flat()

View file

@ -6,10 +6,16 @@
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useTransactionBreakdown } from '../../../hooks/useTransactionBreakdown';
import { TransactionBreakdownGraph } from './TransactionBreakdownGraph';
import { useTransactionBreakdown } from '../../../../hooks/use_transaction_breakdown';
import { TransactionBreakdownChartContents } from './transaction_breakdown_chart_contents';
function TransactionBreakdown() {
export function TransactionBreakdownChart({
height,
showAnnotations = true,
}: {
height?: number;
showAnnotations?: boolean;
}) {
const { data, status } = useTransactionBreakdown();
const { timeseries } = data;
@ -20,20 +26,20 @@ function TransactionBreakdown() {
<EuiTitle size="xs">
<h3>
{i18n.translate('xpack.apm.transactionBreakdown.chartTitle', {
defaultMessage: 'Time spent by span type',
defaultMessage: 'Average duration by span type',
})}
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TransactionBreakdownGraph
timeseries={timeseries}
<TransactionBreakdownChartContents
fetchStatus={status}
height={height}
showAnnotations={showAnnotations}
timeseries={timeseries}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}
export { TransactionBreakdown };

View file

@ -18,6 +18,7 @@ import {
import moment from 'moment';
import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { useChartTheme } from '../../../../../../observability/public';
import { asPercent } from '../../../../../common/utils/formatters';
import { TimeSeries } from '../../../../../typings/timeseries';
import { FETCH_STATUS } from '../../../../hooks/useFetcher';
@ -28,16 +29,22 @@ import { Annotations } from '../../charts/annotations';
import { ChartContainer } from '../../charts/chart_container';
import { onBrushEnd } from '../../charts/helper/helper';
const XY_HEIGHT = unit * 16;
interface Props {
fetchStatus: FETCH_STATUS;
height?: number;
showAnnotations: boolean;
timeseries?: TimeSeries[];
}
export function TransactionBreakdownGraph({ fetchStatus, timeseries }: Props) {
export function TransactionBreakdownChartContents({
fetchStatus,
height = unit * 16,
showAnnotations,
timeseries,
}: Props) {
const history = useHistory();
const chartRef = React.createRef<Chart>();
const chartTheme = useChartTheme();
const { event, setEvent } = useChartsSync2();
const { urlParams } = useUrlParams();
const { start, end } = urlParams;
@ -54,17 +61,14 @@ export function TransactionBreakdownGraph({ fetchStatus, timeseries }: Props) {
const xFormatter = niceTimeFormatter([min, max]);
return (
<ChartContainer
height={XY_HEIGHT}
hasData={!!timeseries}
status={fetchStatus}
>
<ChartContainer height={height} hasData={!!timeseries} status={fetchStatus}>
<Chart ref={chartRef} id="timeSpentBySpan">
<Settings
onBrushEnd={({ x }) => onBrushEnd({ x, history })}
showLegend
showLegendExtra
legendPosition={Position.Bottom}
theme={chartTheme}
xDomain={{ min, max }}
flatLegend
onPointerUpdate={(currEvent: any) => {
@ -87,7 +91,7 @@ export function TransactionBreakdownGraph({ fetchStatus, timeseries }: Props) {
tickFormat={(y: number) => asPercent(y ?? 0, 1)}
/>
<Annotations />
{showAnnotations && <Annotations />}
{timeseries?.length ? (
timeseries.map((serie) => {

View file

@ -28,7 +28,7 @@ import { IUrlParams } from '../../../../context/UrlParamsContext/types';
import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import { ITransactionChartData } from '../../../../selectors/chart_selectors';
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
import { TransactionBreakdown } from '../../TransactionBreakdown';
import { TransactionBreakdownChart } from '../transaction_breakdown_chart';
import { TimeseriesChart } from '../timeseries_chart';
import { TransactionErrorRateChart } from '../transaction_error_rate_chart/';
import { getResponseTimeTickFormatter } from './helper';
@ -117,7 +117,7 @@ export function TransactionCharts({
<TransactionErrorRateChart />
</EuiFlexItem>
<EuiFlexItem>
<TransactionBreakdown />
<TransactionBreakdownChart />
</EuiFlexItem>
</EuiFlexGrid>
</ChartsSyncContextProvider>

View file

@ -7,13 +7,13 @@
import { useParams } from 'react-router-dom';
import { useFetcher } from './useFetcher';
import { useUrlParams } from './useUrlParams';
import { useTransactionType } from './use_transaction_type';
export function useTransactionBreakdown() {
const { serviceName } = useParams<{ serviceName?: string }>();
const {
urlParams: { start, end, transactionName, transactionType },
uiFilters,
} = useUrlParams();
const { urlParams, uiFilters } = useUrlParams();
const { start, end, transactionName } = urlParams;
const transactionType = useTransactionType();
const { data = { timeseries: undefined }, error, status } = useFetcher(
(callApmApi) => {

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { getFirstTransactionType } from '../../common/agent_name';
import { useAgentName } from './useAgentName';
import { useServiceTransactionTypes } from './useServiceTransactionTypes';
import { useUrlParams } from './useUrlParams';
/**
* Get either the transaction type from the URL parameters, "request"
* (for non-RUM agents), "page-load" (for RUM agents) if this service uses them,
* or the first available transaction type.
*/
export function useTransactionType() {
const { agentName } = useAgentName();
const { urlParams } = useUrlParams();
const transactionTypeFromUrlParams = urlParams.transactionType;
const transactionTypes = useServiceTransactionTypes(urlParams);
const firstTransactionType = getFirstTransactionType(
transactionTypes,
agentName
);
return transactionTypeFromUrlParams ?? firstTransactionType;
}

View file

@ -8,5 +8,19 @@ import { useTheme } from './use_theme';
export function useChartTheme() {
const theme = useTheme();
return theme.darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme;
const baseChartTheme = theme.darkMode
? EUI_CHARTS_THEME_DARK.theme
: EUI_CHARTS_THEME_LIGHT.theme;
return {
...baseChartTheme,
background: {
...baseChartTheme.background,
color: 'transparent',
},
lineSeriesStyle: {
...baseChartTheme.lineSeriesStyle,
point: { visible: false },
},
};
}