diff --git a/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
index 0521270a7ba74..9d82cd6b5455c 100644
--- a/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
+++ b/x-pack/legacy/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
@@ -102,6 +102,8 @@ exports[`Error TRANSACTION_TYPE 1`] = `"request"`;
exports[`Error URL_FULL 1`] = `undefined`;
+exports[`Error USER_AGENT_NAME 1`] = `undefined`;
+
exports[`Error USER_ID 1`] = `undefined`;
exports[`Span CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
@@ -206,6 +208,8 @@ exports[`Span TRANSACTION_TYPE 1`] = `undefined`;
exports[`Span URL_FULL 1`] = `undefined`;
+exports[`Span USER_AGENT_NAME 1`] = `undefined`;
+
exports[`Span USER_ID 1`] = `undefined`;
exports[`Transaction CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
@@ -310,4 +314,6 @@ exports[`Transaction TRANSACTION_TYPE 1`] = `"transaction type"`;
exports[`Transaction URL_FULL 1`] = `"http://www.elastic.co"`;
+exports[`Transaction USER_AGENT_NAME 1`] = `"Other"`;
+
exports[`Transaction USER_ID 1`] = `"1337"`;
diff --git a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.test.ts b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.test.ts
index da637cf8c7714..c5325dba0fd1b 100644
--- a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.test.ts
+++ b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.test.ts
@@ -33,6 +33,7 @@ describe('Transaction', () => {
timestamp: { us: 1337 },
trace: { id: 'trace id' },
user: { id: '1337' },
+ user_agent: { name: 'Other', original: 'test original' },
parent: {
id: 'parentId'
},
diff --git a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts
index 552c149ce6214..d0830337e0d35 100644
--- a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts
+++ b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts
@@ -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';
diff --git a/x-pack/legacy/plugins/apm/common/processor_event.ts b/x-pack/legacy/plugins/apm/common/processor_event.ts
index a513f62092767..83dadfc21da90 100644
--- a/x-pack/legacy/plugins/apm/common/processor_event.ts
+++ b/x-pack/legacy/plugins/apm/common/processor_event.ts
@@ -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'
+}
diff --git a/x-pack/legacy/plugins/apm/common/transaction_types.ts b/x-pack/legacy/plugins/apm/common/transaction_types.ts
index 4dd59af63047d..1226e926b1ee3 100644
--- a/x-pack/legacy/plugins/apm/common/transaction_types.ts
+++ b/x-pack/legacy/plugins/apm/common/transaction_types.ts
@@ -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';
diff --git a/x-pack/legacy/plugins/apm/common/viz_colors.ts b/x-pack/legacy/plugins/apm/common/viz_colors.ts
new file mode 100644
index 0000000000000..cc070005409b6
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/common/viz_colors.ts
@@ -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];
+}
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx
new file mode 100644
index 0000000000000..e95f733fb4bc8
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx
@@ -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()).not.toThrowError();
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx
new file mode 100644
index 0000000000000..58bc4655f730c
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx
@@ -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 (
+ <>
+
+
+ {i18n.translate(
+ 'xpack.apm.metrics.pageLoadCharts.avgPageLoadByBrowser',
+ {
+ defaultMessage: 'Avg. page load duration distribution by browser'
+ }
+ )}
+
+
+
+ >
+ );
+}
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx
index 6176397170797..bea858d1358c5 100644
--- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx
@@ -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 (
-
-
-
-
-
- {i18n.translate(
- 'xpack.apm.metrics.durationByCountryMap.avgPageLoadByCountryLabel',
- {
- defaultMessage:
- 'Avg. page load duration distribution by country'
- }
- )}
-
-
-
-
-
-
+ <>
+ {' '}
+
+
+ {i18n.translate(
+ 'xpack.apm.metrics.durationByCountryMap.avgPageLoadByCountryLabel',
+ {
+ defaultMessage: 'Avg. page load duration distribution by country'
+ }
+ )}
+
+
+
+ >
);
};
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
index c032d60359903..97794bf66687b 100644
--- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
@@ -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,
@@ -59,31 +60,29 @@ const ShiftedEuiText = styled(EuiText)`
top: 5px;
`;
-export class TransactionCharts extends Component {
- 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 {
public getTPMFormatter = (t: number) => {
const { urlParams } = this.props;
const unit = tpmUnit(urlParams.transactionType);
@@ -154,7 +153,7 @@ export class TransactionCharts extends Component {
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 (
@@ -177,8 +176,8 @@ export class TransactionCharts extends Component {
@@ -205,7 +204,18 @@ export class TransactionCharts extends Component {
{transactionType === TRANSACTION_PAGE_LOAD && (
<>
-
+
+
+
+
+
+
+
+
+
+
+
+
>
)}
>
diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts
index 5fa9294a95dfd..1806e7395a8cc 100644
--- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts
+++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts
@@ -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':
@@ -113,7 +113,7 @@ export function getPathParams(pathname: string = ''): PathParams {
case 'traces':
return {
- processorEvent: 'transaction'
+ processorEvent: ProcessorEvent.transaction
};
default:
return {};
diff --git a/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts b/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts
new file mode 100644
index 0000000000000..38f26c2ba9fbd
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts
@@ -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'
+ }
+ ]);
+ });
+});
diff --git a/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.ts b/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.ts
new file mode 100644
index 0000000000000..a1e9294455d54
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.ts
@@ -0,0 +1,60 @@
+/*
+ * 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';
+import { getVizColorForIndex } from '../../common/viz_colors';
+
+function toTimeSeries(data?: AvgDurationByBrowserAPIResponse): TimeSeries[] {
+ if (!data) {
+ return [];
+ }
+
+ return data.map((item, index) => {
+ return {
+ ...item,
+ color: getVizColorForIndex(index, theme),
+ 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
+ };
+}
diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/fetchAndTransformGcMetrics.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/fetchAndTransformGcMetrics.ts
index 180537d68a2a2..8cff6e5d3aa80 100644
--- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/fetchAndTransformGcMetrics.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/fetchAndTransformGcMetrics.ts
@@ -23,16 +23,7 @@ import {
METRIC_JAVA_GC_TIME
} from '../../../../../../common/elasticsearch_fieldnames';
import { getBucketSize } from '../../../../helpers/get_bucket_size';
-
-const colors = [
- theme.euiColorVis0,
- theme.euiColorVis1,
- theme.euiColorVis2,
- theme.euiColorVis3,
- theme.euiColorVis4,
- theme.euiColorVis5,
- theme.euiColorVis6
-];
+import { getVizColorForIndex } from '../../../../../../common/viz_colors';
export async function fetchAndTransformGcMetrics({
setup,
@@ -148,7 +139,7 @@ export async function fetchAndTransformGcMetrics({
title: label,
key: label,
type: chartBase.type,
- color: colors[i],
+ color: getVizColorForIndex(i, theme),
overallValue,
data
};
diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts
index 1e7f197435a67..03f21e4f26e7b 100644
--- a/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts
@@ -11,16 +11,7 @@ import {
ESSearchRequest
} from '../../../typings/elasticsearch';
import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations';
-
-const colors = [
- theme.euiColorVis0,
- theme.euiColorVis1,
- theme.euiColorVis2,
- theme.euiColorVis3,
- theme.euiColorVis4,
- theme.euiColorVis5,
- theme.euiColorVis6
-];
+import { getVizColorForIndex } from '../../../common/viz_colors';
export type GenericMetricsChart = ReturnType<
typeof transformDataToMetricsChart
@@ -66,7 +57,8 @@ export function transformDataToMetricsChart(
title: chartBase.series[seriesKey].title,
key: seriesKey,
type: chartBase.type,
- color: chartBase.series[seriesKey].color || colors[i],
+ color:
+ chartBase.series[seriesKey].color || getVizColorForIndex(i, theme),
overallValue,
data:
timeseriesData?.buckets.map(bucket => {
diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/__fixtures__/responses.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/__fixtures__/responses.ts
new file mode 100644
index 0000000000000..3f0f8a84dc62f
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/__fixtures__/responses.ts
@@ -0,0 +1,74 @@
+/*
+ * 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 {
+ ESSearchResponse,
+ ESSearchRequest
+} from '../../../../../typings/elasticsearch';
+
+export const response = ({
+ hits: {
+ total: 599,
+ max_score: 0,
+ hits: []
+ },
+ took: 4,
+ timed_out: false,
+ _shards: {
+ total: 1,
+ successful: 1,
+ skipped: 0,
+ failed: 0
+ },
+ aggregations: {
+ user_agent_keys: {
+ buckets: [{ key: 'Firefox' }, { key: 'Other' }]
+ },
+ browsers: {
+ buckets: [
+ {
+ key_as_string: '2019-10-21T04:38:20.000-05:00',
+ key: 1571650700000,
+ doc_count: 0,
+ user_agent: {
+ doc_count_error_upper_bound: 0,
+ sum_other_doc_count: 0,
+ buckets: []
+ }
+ },
+ {
+ key_as_string: '2019-10-21T04:40:00.000-05:00',
+ key: 1571650800000,
+ doc_count: 1,
+ user_agent: {
+ doc_count_error_upper_bound: 0,
+ sum_other_doc_count: 0,
+ buckets: [
+ {
+ key: 'Other',
+ doc_count: 1,
+ avg_duration: {
+ value: 860425.0
+ }
+ },
+ {
+ key: 'Firefox',
+ doc_count: 10,
+ avg_duration: {
+ value: 86425.1
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+} as unknown) as ESSearchResponse<
+ unknown,
+ ESSearchRequest,
+ { restTotalHitsAsInt: false }
+>;
diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts
new file mode 100644
index 0000000000000..f2227524db081
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts
@@ -0,0 +1,23 @@
+/*
+ * 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 { Setup } from '../../helpers/setup_request';
+import { fetcher } from './fetcher';
+
+describe('fetcher', () => {
+ it('performs a search', async () => {
+ const search = jest.fn();
+ const setup = ({
+ client: { search },
+ indices: {},
+ uiFiltersES: []
+ } as unknown) as Setup;
+
+ await fetcher({ serviceName: 'testServiceName', setup });
+
+ expect(search).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts
new file mode 100644
index 0000000000000..8a96a25aef50e
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts
@@ -0,0 +1,78 @@
+/*
+ * 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 { ESFilter } from '../../../../typings/elasticsearch';
+import { PromiseReturnType } from '../../../../typings/common';
+import {
+ PROCESSOR_EVENT,
+ SERVICE_NAME,
+ TRANSACTION_TYPE,
+ USER_AGENT_NAME,
+ TRANSACTION_DURATION
+} from '../../../../common/elasticsearch_fieldnames';
+import { rangeFilter } from '../../helpers/range_filter';
+import { getBucketSize } from '../../helpers/get_bucket_size';
+import { Options } from '.';
+import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types';
+import { ProcessorEvent } from '../../../../common/processor_event';
+
+export type ESResponse = PromiseReturnType;
+
+export function fetcher(options: Options) {
+ const { end, client, indices, start, uiFiltersES } = options.setup;
+ const { serviceName } = options;
+ const { intervalString } = getBucketSize(start, end, 'auto');
+
+ const filter: ESFilter[] = [
+ { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
+ { term: { [SERVICE_NAME]: serviceName } },
+ { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } },
+ { range: rangeFilter(start, end) },
+ ...uiFiltersES
+ ];
+
+ const params = {
+ index: indices['apm_oss.transactionIndices'],
+ body: {
+ size: 0,
+ query: { bool: { filter } },
+ aggs: {
+ user_agent_keys: {
+ terms: {
+ field: USER_AGENT_NAME
+ }
+ },
+ browsers: {
+ date_histogram: {
+ extended_bounds: {
+ max: end,
+ min: start
+ },
+ field: '@timestamp',
+ fixed_interval: intervalString,
+ min_doc_count: 0
+ },
+ aggs: {
+ user_agent: {
+ terms: {
+ field: USER_AGENT_NAME
+ },
+ aggs: {
+ avg_duration: {
+ avg: {
+ field: TRANSACTION_DURATION
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+
+ return client.search(params);
+}
diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.test.ts
new file mode 100644
index 0000000000000..fe103ade24161
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.test.ts
@@ -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 {
+ getTransactionAvgDurationByBrowser,
+ Options,
+ AvgDurationByBrowserAPIResponse
+} from '.';
+import * as transformerModule from './transformer';
+import * as fetcherModule from './fetcher';
+import { response } from './__fixtures__/responses';
+
+describe('getAvgDurationByBrowser', () => {
+ it('returns a transformed response', async () => {
+ const transformer = jest
+ .spyOn(transformerModule, 'transformer')
+ .mockReturnValueOnce(({} as unknown) as AvgDurationByBrowserAPIResponse);
+ const search = () => {};
+ const options = ({
+ setup: { client: { search }, indices: {}, uiFiltersES: [] }
+ } as unknown) as Options;
+ jest
+ .spyOn<{ fetcher: any }, 'fetcher'>(fetcherModule, 'fetcher')
+ .mockResolvedValueOnce(response);
+
+ await getTransactionAvgDurationByBrowser(options);
+
+ expect(transformer).toHaveBeenCalledWith({ response });
+ });
+});
diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts
new file mode 100644
index 0000000000000..57b3c8cbe9f93
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 { Coordinate } from '../../../../typings/timeseries';
+import { Setup } from '../../helpers/setup_request';
+import { fetcher } from './fetcher';
+import { transformer } from './transformer';
+
+export interface Options {
+ serviceName: string;
+ setup: Setup;
+}
+
+export type AvgDurationByBrowserAPIResponse = Array<{
+ data: Coordinate[];
+ title: string;
+}>;
+
+export async function getTransactionAvgDurationByBrowser(options: Options) {
+ return transformer({ response: await fetcher(options) });
+}
diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.test.ts
new file mode 100644
index 0000000000000..5caec12c81d5d
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.test.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 { transformer } from './transformer';
+import { response } from './__fixtures__/responses';
+
+describe('transformer', () => {
+ it('transforms', () => {
+ expect(transformer({ response })).toEqual([
+ {
+ data: [
+ { x: 1571650700000, y: undefined },
+ { x: 1571650800000, y: 86425.1 }
+ ],
+ title: 'Firefox'
+ },
+ {
+ data: [
+ { x: 1571650700000, y: undefined },
+ { x: 1571650800000, y: 860425.0 }
+ ],
+ title: 'Other'
+ }
+ ]);
+ });
+});
diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts
new file mode 100644
index 0000000000000..805f8f192bdb1
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts
@@ -0,0 +1,55 @@
+/*
+ * 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 { ESResponse } from './fetcher';
+import { AvgDurationByBrowserAPIResponse } from '.';
+import { Coordinate } from '../../../../typings/timeseries';
+
+export function transformer({
+ response
+}: {
+ response: ESResponse;
+}): AvgDurationByBrowserAPIResponse {
+ const allUserAgentKeys = new Set(
+ // TODO(TS-3.7-ESLINT)
+ // eslint-disable-next-line @typescript-eslint/camelcase
+ (response.aggregations?.user_agent_keys?.buckets ?? []).map(({ key }) =>
+ key.toString()
+ )
+ );
+ const buckets = response.aggregations?.browsers?.buckets ?? [];
+
+ const series = buckets.reduce<{ [key: string]: Coordinate[] }>(
+ (acc, next) => {
+ const userAgentBuckets = next.user_agent?.buckets ?? [];
+ const x = next.key;
+ const seenUserAgentKeys = new Set();
+
+ userAgentBuckets.map(userAgentBucket => {
+ const key = userAgentBucket.key;
+ const y = userAgentBucket.avg_duration?.value;
+
+ seenUserAgentKeys.add(key.toString());
+ acc[key] = (acc[key] || []).concat({ x, y });
+ });
+
+ const emptyUserAgents = new Set(
+ [...allUserAgentKeys].filter(key => !seenUserAgentKeys.has(key))
+ );
+
+ // If no user agent requests exist for this bucked, fill in the data with
+ // undefined
+ [...emptyUserAgents].map(key => {
+ acc[key] = (acc[key] || []).concat({ x, y: undefined });
+ });
+
+ return acc;
+ },
+ {}
+ );
+
+ return Object.entries(series).map(([title, data]) => ({ title, data }));
+}
diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/constants.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/constants.ts
index 0e288de1e4600..dcf6e8e07c45b 100644
--- a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/constants.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/constants.ts
@@ -3,19 +3,5 @@
* 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';
export const MAX_KPIS = 20;
-
-export const COLORS = [
- theme.euiColorVis0,
- theme.euiColorVis1,
- theme.euiColorVis2,
- theme.euiColorVis3,
- theme.euiColorVis4,
- theme.euiColorVis5,
- theme.euiColorVis6,
- theme.euiColorVis7,
- theme.euiColorVis8,
- theme.euiColorVis9
-];
diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts
index 3166938090d8f..12f6694116950 100644
--- a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts
@@ -18,7 +18,8 @@ import {
import { Setup } from '../../helpers/setup_request';
import { rangeFilter } from '../../helpers/range_filter';
import { getMetricsDateHistogramParams } from '../../helpers/metrics';
-import { MAX_KPIS, COLORS } from './constants';
+import { MAX_KPIS } from './constants';
+import { getVizColorForIndex } from '../../../../common/viz_colors';
export async function getTransactionBreakdown({
setup,
@@ -142,7 +143,7 @@ export async function getTransactionBreakdown({
const kpis = sortByOrder(visibleKpis, 'name').map((kpi, index) => {
return {
...kpi,
- color: COLORS[index % COLORS.length]
+ color: getVizColorForIndex(index)
};
});
diff --git a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts
index c35b66b453634..1735aa9da7dca 100644
--- a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts
+++ b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts
@@ -42,7 +42,8 @@ import {
transactionGroupsChartsRoute,
transactionGroupsDistributionRoute,
transactionGroupsRoute,
- transactionGroupsAvgDurationByCountry
+ transactionGroupsAvgDurationByCountry,
+ transactionGroupsAvgDurationByBrowser
} from './transaction_groups';
import {
errorGroupsLocalFiltersRoute,
@@ -102,6 +103,7 @@ const createApmApi = () => {
.add(transactionGroupsChartsRoute)
.add(transactionGroupsDistributionRoute)
.add(transactionGroupsRoute)
+ .add(transactionGroupsAvgDurationByBrowser)
.add(transactionGroupsAvgDurationByCountry)
// UI filters
diff --git a/x-pack/legacy/plugins/apm/server/routes/transaction_groups.ts b/x-pack/legacy/plugins/apm/server/routes/transaction_groups.ts
index 0b5c29fc29857..269f5fee9738c 100644
--- a/x-pack/legacy/plugins/apm/server/routes/transaction_groups.ts
+++ b/x-pack/legacy/plugins/apm/server/routes/transaction_groups.ts
@@ -12,6 +12,7 @@ import { getTransactionBreakdown } from '../lib/transactions/breakdown';
import { getTransactionGroupList } from '../lib/transaction_groups';
import { createRoute } from './create_route';
import { uiFiltersRt, rangeRt } from './default_api_types';
+import { getTransactionAvgDurationByBrowser } from '../lib/transactions/avg_duration_by_browser';
import { getTransactionAvgDurationByCountry } from '../lib/transactions/avg_duration_by_country';
export const transactionGroupsRoute = createRoute(() => ({
@@ -144,6 +145,32 @@ export const transactionGroupsBreakdownRoute = createRoute(() => ({
}
}));
+export const transactionGroupsAvgDurationByBrowser = createRoute(() => ({
+ path: `/api/apm/services/{serviceName}/transaction_groups/avg_duration_by_browser`,
+ params: {
+ path: t.type({
+ serviceName: t.string
+ }),
+ query: t.intersection([
+ t.partial({
+ transactionType: t.string,
+ transactionName: t.string
+ }),
+ uiFiltersRt,
+ rangeRt
+ ])
+ },
+ handler: async (req, { path }) => {
+ const setup = await setupRequest(req);
+ const { serviceName } = path;
+
+ return getTransactionAvgDurationByBrowser({
+ serviceName,
+ setup
+ });
+ }
+}));
+
export const transactionGroupsAvgDurationByCountry = createRoute(() => ({
path: `/api/apm/services/{serviceName}/transaction_groups/avg_duration_by_country`,
params: {