Skip to content
/ kibana Public
forked from elastic/kibana

Commit

Permalink
[APM] Performance comparison charts by browser
Browse files Browse the repository at this point in the history
Show a chart with average page load broken down by user agent name on the RUM overview.

I tested this by setting up a RUM agent on a sample app on my test cloud cluster.

On the internal APM dev cluster you'll need to set the time range to about 90 days then look at the "client" app and find the time range where there are requests. Unfortunately with this sample data the only series you see is "Other" because there aren't a lot of requests.

Fixes elastic#43342
  • Loading branch information
smith committed Nov 20, 2019
1 parent 7fddc23 commit bb4ec15
Show file tree
Hide file tree
Showing 17 changed files with 569 additions and 44 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const SERVICE_NODE_NAME = 'service.node.name';
export const URL_FULL = 'url.full';
export const HTTP_REQUEST_METHOD = 'http.request.method';
export const USER_ID = 'user.id';
export const USER_AGENT_NAME = 'user_agent.name';

export const OBSERVER_VERSION_MAJOR = 'observer.version_major';
export const OBSERVER_LISTENING = 'observer.listening';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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 React from 'react';
import { shallow } from 'enzyme';
import { BrowserLineChart } from './BrowserLineChart';

describe('BrowserLineChart', () => {
describe('render', () => {
it('renders', () => {
const props = { series: [] };

expect(() => shallow(<BrowserLineChart {...props} />)).not.toThrowError();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiTitle } from '@elastic/eui';
import { TransactionLineChart } from './TransactionLineChart';
import {
getMaxY,
getResponseTimeTickFormatter,
getResponseTimeTooltipFormatter
} from '.';
import { getDurationFormatter } from '../../../../utils/formatters';
import { useAvgDurationByBrowser } from '../../../../hooks/useAvgDurationByBrowser';

export function BrowserLineChart() {
const { data } = useAvgDurationByBrowser();
const maxY = getMaxY(data);
const formatter = getDurationFormatter(maxY);
const formatTooltipValue = getResponseTimeTooltipFormatter(formatter);
const tickFormatY = getResponseTimeTickFormatter(formatter);

return (
<>
<EuiTitle size="xs">
<span>
{i18n.translate(
'xpack.apm.metrics.pageLoadCharts.avgPageLoadByBrowser',
{
defaultMessage: 'Avg. page load duration distribution by browser'
}
)}
</span>
</EuiTitle>
<TransactionLineChart
formatTooltipValue={formatTooltipValue}
series={data}
tickFormatY={tickFormatY}
/>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,30 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { EuiFlexGrid, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
import { EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useAvgDurationByCountry } from '../../../../../hooks/useAvgDurationByCountry';

import { ChoroplethMap } from '../ChoroplethMap';

export const DurationByCountryMap: React.SFC = () => {
const { data } = useAvgDurationByCountry();

return (
<EuiFlexGrid columns={1} gutterSize="s">
<EuiFlexItem>
<EuiPanel>
<EuiTitle size="xs">
<span>
{i18n.translate(
'xpack.apm.metrics.durationByCountryMap.avgPageLoadByCountryLabel',
{
defaultMessage:
'Avg. page load duration distribution by country'
}
)}
</span>
</EuiTitle>
<ChoroplethMap items={data} />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGrid>
<>
{' '}
<EuiTitle size="xs">
<span>
{i18n.translate(
'xpack.apm.metrics.durationByCountryMap.avgPageLoadByCountryLabel',
{
defaultMessage: 'Avg. page load duration distribution by country'
}
)}
</span>
</EuiTitle>
<ChoroplethMap items={data} />
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink';
import { LicenseContext } from '../../../../context/LicenseContext';
import { TransactionLineChart } from './TransactionLineChart';
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
import { BrowserLineChart } from './BrowserLineChart';
import { DurationByCountryMap } from './DurationByCountryMap';
import {
TRANSACTION_PAGE_LOAD,
Expand All @@ -59,31 +60,29 @@ const ShiftedEuiText = styled(EuiText)`
top: 5px;
`;

export class TransactionCharts extends Component<TransactionChartProps> {
public getMaxY = (responseTimeSeries: TimeSeries[]) => {
const coordinates = flatten(
responseTimeSeries.map((serie: TimeSeries) => serie.data as Coordinate[])
);

const numbers: number[] = coordinates.map((c: Coordinate) =>
c.y ? c.y : 0
);
export function getResponseTimeTickFormatter(formatter: TimeFormatter) {
return (t: number) => formatter(t).formatted;
}

return Math.max(...numbers, 0);
export function getResponseTimeTooltipFormatter(formatter: TimeFormatter) {
return (p: Coordinate) => {
return isValidCoordinateValue(p.y)
? formatter(p.y).formatted
: NOT_AVAILABLE_LABEL;
};
}

public getResponseTimeTickFormatter = (formatter: TimeFormatter) => {
return (t: number) => formatter(t).formatted;
};
export function getMaxY(responseTimeSeries: TimeSeries[]) {
const coordinates = flatten(
responseTimeSeries.map((serie: TimeSeries) => serie.data as Coordinate[])
);

public getResponseTimeTooltipFormatter = (formatter: TimeFormatter) => {
return (p: Coordinate) => {
return isValidCoordinateValue(p.y)
? formatter(p.y).formatted
: NOT_AVAILABLE_LABEL;
};
};
const numbers: number[] = coordinates.map((c: Coordinate) => (c.y ? c.y : 0));

return Math.max(...numbers, 0);
}

export class TransactionCharts extends Component<TransactionChartProps> {
public getTPMFormatter = (t: number) => {
const { urlParams } = this.props;
const unit = tpmUnit(urlParams.transactionType);
Expand Down Expand Up @@ -154,7 +153,7 @@ export class TransactionCharts extends Component<TransactionChartProps> {
const { charts, urlParams } = this.props;
const { responseTimeSeries, tpmSeries } = charts;
const { transactionType } = urlParams;
const maxY = this.getMaxY(responseTimeSeries);
const maxY = getMaxY(responseTimeSeries);
const formatter = getDurationFormatter(maxY);

return (
Expand All @@ -177,8 +176,8 @@ export class TransactionCharts extends Component<TransactionChartProps> {
</EuiFlexGroup>
<TransactionLineChart
series={responseTimeSeries}
tickFormatY={this.getResponseTimeTickFormatter(formatter)}
formatTooltipValue={this.getResponseTimeTooltipFormatter(
tickFormatY={getResponseTimeTickFormatter(formatter)}
formatTooltipValue={getResponseTimeTooltipFormatter(
formatter
)}
/>
Expand All @@ -205,7 +204,18 @@ export class TransactionCharts extends Component<TransactionChartProps> {
{transactionType === TRANSACTION_PAGE_LOAD && (
<>
<EuiSpacer size="s" />
<DurationByCountryMap />
<EuiFlexGrid columns={2} gutterSize="s">
<EuiFlexItem>
<EuiPanel>
<DurationByCountryMap />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel>
<BrowserLineChart />
</EuiPanel>
</EuiFlexItem>
</EuiFlexGrid>
</>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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 { renderHook } from 'react-hooks-testing-library';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import * as useFetcherModule from './useFetcher';
import { useAvgDurationByBrowser } from './useAvgDurationByBrowser';

describe('useAvgDurationByBrowser', () => {
it('returns data', () => {
const data = [
{ title: 'Other', data: [{ x: 1572530100000, y: 130010.8947368421 }] }
];
jest.spyOn(useFetcherModule, 'useFetcher').mockReturnValueOnce({
data,
refetch: () => {},
status: 'success' as useFetcherModule.FETCH_STATUS
});
const { result } = renderHook(() => useAvgDurationByBrowser());

expect(result.current.data).toEqual([
{
color: theme.euiColorVis0,
data: [{ x: 1572530100000, y: 130010.8947368421 }],
title: 'Other',
type: 'linemark'
}
]);
});
});
72 changes: 72 additions & 0 deletions x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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 theme from '@elastic/eui/dist/eui_theme_light.json';
import { useFetcher } from './useFetcher';
import { useUrlParams } from './useUrlParams';
import { AvgDurationByBrowserAPIResponse } from '../../server/lib/transactions/avg_duration_by_browser';
import { TimeSeries } from '../../typings/timeseries';

const colors = [
theme.euiColorVis0,
theme.euiColorVis1,
theme.euiColorVis2,
theme.euiColorVis3,
theme.euiColorVis4,
theme.euiColorVis5,
theme.euiColorVis6,
theme.euiColorVis7,
theme.euiColorVis8,
theme.euiColorVis9
];

function toTimeSeries(data: AvgDurationByBrowserAPIResponse): TimeSeries[] {
if (!data) {
return [];
}

return data.map((item, index) => {
return {
...item,
color: colors[index % colors.length],
type: 'linemark'
};
});
}

export function useAvgDurationByBrowser() {
const {
urlParams: { serviceName, start, end, transactionName },
uiFilters
} = useUrlParams();

const { data, error, status } = useFetcher(
callApmApi => {
if (serviceName && start && end) {
return callApmApi({
pathname:
'/api/apm/services/{serviceName}/transaction_groups/avg_duration_by_browser',
params: {
path: { serviceName },
query: {
start,
end,
transactionName,
uiFilters: JSON.stringify(uiFilters)
}
}
});
}
},
[serviceName, start, end, transactionName, uiFilters]
);

return {
data: toTimeSeries(data),
status,
error
};
}
Loading

0 comments on commit bb4ec15

Please sign in to comment.