Skip to content

Commit

Permalink
[APM] Performance comparison charts by user agent (browser) (#49582)
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.

Also factor out the color selection into a common helper.

Fixes #43342
  • Loading branch information
smith authored Nov 20, 2019
1 parent 052745e commit 35b0362
Show file tree
Hide file tree
Showing 26 changed files with 605 additions and 89 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 @@ -34,6 +34,7 @@ describe('Transaction', () => {
timestamp: { us: 1337 },
trace: { id: 'trace id' },
user: { id: '1337' },
user_agent: { name: 'Other', original: 'test original' },
parent: {
id: 'parentId'
},
Expand Down
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
6 changes: 5 additions & 1 deletion x-pack/legacy/plugins/apm/common/processor_event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

export type ProcessorEvent = 'transaction' | 'error' | 'metric';
export enum ProcessorEvent {
transaction = 'transaction',
error = 'error',
metric = 'metric'
}
2 changes: 1 addition & 1 deletion x-pack/legacy/plugins/apm/common/transaction_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
*/

export const TRANSACTION_PAGE_LOAD = 'page-load';
export const TRANSACTION_ROUTE_CHANGE = 'route-change';
export const TRANSACTION_REQUEST = 'request';
export const TRANSACTION_ROUTE_CHANGE = 'route-change';
27 changes: 27 additions & 0 deletions x-pack/legacy/plugins/apm/common/viz_colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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 lightTheme from '@elastic/eui/dist/eui_theme_light.json';

function getVizColorsForTheme(theme = lightTheme) {
return [
theme.euiColorVis0,
theme.euiColorVis1,
theme.euiColorVis2,
theme.euiColorVis3,
theme.euiColorVis4,
theme.euiColorVis5,
theme.euiColorVis6,
theme.euiColorVis7,
theme.euiColorVis8,
theme.euiColorVis9
];
}

export function getVizColorForIndex(index = 0, theme = lightTheme) {
const colors = getVizColorsForTheme(theme);
return colors[index % colors.length];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* 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', () => {
expect(() => shallow(<BrowserLineChart />)).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
Expand Up @@ -83,24 +83,24 @@ export function getPathParams(pathname: string = ''): PathParams {
switch (servicePageName) {
case 'transactions':
return {
processorEvent: 'transaction',
processorEvent: ProcessorEvent.transaction,
serviceName
};
case 'errors':
return {
processorEvent: 'error',
processorEvent: ProcessorEvent.error,
serviceName,
errorGroupId: paths[3]
};
case 'metrics':
return {
processorEvent: 'metric',
processorEvent: ProcessorEvent.metric,
serviceName,
serviceNodeName
};
case 'nodes':
return {
processorEvent: 'metric',
processorEvent: ProcessorEvent.metric,
serviceName
};
case 'service-map':
Expand All @@ -113,7 +113,7 @@ export function getPathParams(pathname: string = ''): PathParams {

case 'traces':
return {
processorEvent: 'transaction'
processorEvent: ProcessorEvent.transaction
};
default:
return {};
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'
}
]);
});
});
Loading

0 comments on commit 35b0362

Please sign in to comment.