* [APM] Adds chart for page load averages by country in RUM page-load view * [APM] Simplified and refined ChoroplethMap. Added legend labels. * - Replaced Map legend slices with smooth gradient - fixed issue with map rendering multiple times - renamed initial props to start with 'initial' - added some more code comments * use correct i18n ids * - base color progression calc directly on euiColorPrimary - check that a layer exists before querying features * Addressed code review feedback * - fixes issue where min/max was not a finite value - cleans up mouseover handler, which updates on state changes - formats doc count for display - style improvements * addressed PR feedback & updated renovate.json5 * - Removed the Legend from the ChoroplethMap - Only render tooltip when there's data * - fix hover state not clearing properly - add better typing around Geojson propertier for world countries * added missing css import
This commit is contained in:
parent
4403ed3de9
commit
dfe7eb6b3d
|
@ -641,6 +641,14 @@
|
|||
'@types/jsonwebtoken',
|
||||
],
|
||||
},
|
||||
{
|
||||
groupSlug: 'mapbox-gl',
|
||||
groupName: 'mapbox-gl related packages',
|
||||
packageNames: [
|
||||
'mapbox-gl',
|
||||
'@types/mapbox-gl',
|
||||
],
|
||||
},
|
||||
{
|
||||
groupSlug: 'memoize-one',
|
||||
groupName: 'memoize-one related packages',
|
||||
|
|
|
@ -13,3 +13,6 @@ bluebird.Promise.setScheduler(function (fn) { global.setImmediate.call(global, f
|
|||
|
||||
const MutationObserver = require('mutation-observer');
|
||||
Object.defineProperty(window, 'MutationObserver', { value: MutationObserver });
|
||||
|
||||
const URL = { createObjectURL: () => '' };
|
||||
Object.defineProperty(window, 'URL', { value: URL });
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Error CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
|
||||
|
||||
exports[`Error CONTAINER_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Error ERROR_CULPRIT 1`] = `"handleOopsie"`;
|
||||
|
@ -92,6 +94,8 @@ exports[`Error URL_FULL 1`] = `undefined`;
|
|||
|
||||
exports[`Error USER_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Span CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
|
||||
|
||||
exports[`Span CONTAINER_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Span ERROR_CULPRIT 1`] = `undefined`;
|
||||
|
@ -184,6 +188,8 @@ exports[`Span URL_FULL 1`] = `undefined`;
|
|||
|
||||
exports[`Span USER_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction CONTAINER_ID 1`] = `"container1234567890abcdef"`;
|
||||
|
||||
exports[`Transaction ERROR_CULPRIT 1`] = `undefined`;
|
||||
|
|
|
@ -61,3 +61,5 @@ export const METRIC_JAVA_THREAD_COUNT = 'jvm.thread.count';
|
|||
export const HOST_NAME = 'host.hostname';
|
||||
export const CONTAINER_ID = 'container.id';
|
||||
export const POD_NAME = 'kubernetes.pod.name';
|
||||
|
||||
export const CLIENT_GEO_COUNTRY_ISO_CODE = 'client.geo.country_iso_code';
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { asTime, asInteger } from '../../../../../utils/formatters';
|
||||
import { fontSizes } from '../../../../../style/variables';
|
||||
|
||||
export const ChoroplethToolTip: React.SFC<{
|
||||
name: string;
|
||||
value: number;
|
||||
docCount: number;
|
||||
}> = ({ name, value, docCount }) => {
|
||||
return (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: fontSizes.large }}>{name}</div>
|
||||
<div>
|
||||
{i18n.translate(
|
||||
'xpack.apm.metrics.pageLoadCharts.RegionMapChart.ToolTip.avgPageLoadDuration',
|
||||
{
|
||||
defaultMessage: 'Avg. page load duration:'
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontWeight: 'bold', fontSize: fontSizes.large }}>
|
||||
{asTime(value)}
|
||||
</div>
|
||||
<div>
|
||||
(
|
||||
{i18n.translate(
|
||||
'xpack.apm.metrics.pageLoadCharts.RegionMapChart.ToolTip.countPageLoads',
|
||||
{
|
||||
values: { docCount: asInteger(docCount) },
|
||||
defaultMessage: '{docCount} page loads'
|
||||
}
|
||||
)}
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,269 @@
|
|||
/*
|
||||
* 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, {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
useMemo
|
||||
} from 'react';
|
||||
import { Map, NavigationControl, Popup } from 'mapbox-gl';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { shade, tint } from 'polished';
|
||||
import { ChoroplethToolTip } from './ChoroplethToolTip';
|
||||
|
||||
interface ChoroplethItem {
|
||||
key: string;
|
||||
value: number;
|
||||
docCount: number;
|
||||
}
|
||||
|
||||
interface Tooltip {
|
||||
name: string;
|
||||
value: number;
|
||||
docCount: number;
|
||||
}
|
||||
|
||||
interface WorldCountryFeatureProperties {
|
||||
name: string;
|
||||
iso2: string;
|
||||
iso3: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
items: ChoroplethItem[];
|
||||
}
|
||||
|
||||
const CHOROPLETH_LAYER_ID = 'choropleth_layer';
|
||||
const CHOROPLETH_POLYGONS_SOURCE_ID = 'choropleth_polygons';
|
||||
const GEOJSON_KEY_PROPERTY = 'iso2';
|
||||
const MAPBOX_STYLE =
|
||||
'https://tiles.maps.elastic.co/styles/osm-bright-desaturated/style.json';
|
||||
const GEOJSON_SOURCE =
|
||||
'https://vector.maps.elastic.co/files/world_countries_v1.geo.json?elastic_tile_service_tos=agree&my_app_name=ems-landing&my_app_version=7.2.0';
|
||||
|
||||
export function getProgressionColor(scale: number) {
|
||||
const baseColor = euiLightVars.euiColorPrimary;
|
||||
const adjustedScale = 0.75 * scale + 0.05; // prevents pure black & white as min/max colors.
|
||||
if (adjustedScale < 0.5) {
|
||||
return tint(adjustedScale * 2, baseColor);
|
||||
}
|
||||
if (adjustedScale > 0.5) {
|
||||
return shade(1 - (adjustedScale - 0.5) * 2, baseColor);
|
||||
}
|
||||
return baseColor;
|
||||
}
|
||||
|
||||
const getMin = (items: ChoroplethItem[]) =>
|
||||
Math.min(...items.map(item => item.value));
|
||||
|
||||
const getMax = (items: ChoroplethItem[]) =>
|
||||
Math.max(...items.map(item => item.value));
|
||||
|
||||
export const ChoroplethMap: React.SFC<Props> = props => {
|
||||
const { items } = props;
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [map, setMap] = useState<Map | null>(null);
|
||||
const popupRef = useRef<Popup | null>(null);
|
||||
const popupContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [tooltipState, setTooltipState] = useState<Tooltip | null>(null);
|
||||
const [min, max] = useMemo(() => [getMin(items), getMax(items)], [items]);
|
||||
|
||||
// converts an item value to a scaled value between 0 and 1
|
||||
const getValueScale = useCallback(
|
||||
(value: number) => (value - min) / (max - min),
|
||||
[max, min]
|
||||
);
|
||||
|
||||
const controlScrollZoomOnWheel = useCallback((event: WheelEvent) => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
event.preventDefault();
|
||||
} else {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// side effect creates a new mouseover handler referencing new component state
|
||||
// and replaces the old one stored in `updateTooltipStateOnMousemoveRef`
|
||||
useEffect(() => {
|
||||
const updateTooltipStateOnMousemove = (event: mapboxgl.MapMouseEvent) => {
|
||||
const isMapQueryable =
|
||||
map &&
|
||||
popupRef.current &&
|
||||
items.length &&
|
||||
map.getLayer(CHOROPLETH_LAYER_ID);
|
||||
|
||||
if (!isMapQueryable) {
|
||||
return;
|
||||
}
|
||||
(popupRef.current as Popup).setLngLat(event.lngLat);
|
||||
const hoverFeatures = (map as Map).queryRenderedFeatures(event.point, {
|
||||
layers: [CHOROPLETH_LAYER_ID]
|
||||
});
|
||||
|
||||
if (tooltipState && hoverFeatures.length === 0) {
|
||||
return setTooltipState(null);
|
||||
}
|
||||
|
||||
const featureProperties = hoverFeatures[0]
|
||||
.properties as WorldCountryFeatureProperties;
|
||||
|
||||
if (tooltipState && tooltipState.name === featureProperties.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = items.find(
|
||||
({ key }) =>
|
||||
featureProperties && key === featureProperties[GEOJSON_KEY_PROPERTY]
|
||||
);
|
||||
|
||||
if (item) {
|
||||
return setTooltipState({
|
||||
name: featureProperties.name,
|
||||
value: item.value,
|
||||
docCount: item.docCount
|
||||
});
|
||||
}
|
||||
|
||||
setTooltipState(null);
|
||||
};
|
||||
updateTooltipStateOnMousemoveRef.current = updateTooltipStateOnMousemove;
|
||||
}, [map, items, tooltipState]);
|
||||
|
||||
const updateTooltipStateOnMousemoveRef = useRef(
|
||||
(event: mapboxgl.MapMouseEvent & mapboxgl.EventData) => {}
|
||||
);
|
||||
|
||||
// initialization side effect, only runs once
|
||||
useEffect(() => {
|
||||
if (containerRef.current === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// set up Map object
|
||||
const mapboxMap = new Map({
|
||||
attributionControl: false,
|
||||
container: containerRef.current,
|
||||
dragRotate: false,
|
||||
touchZoomRotate: false,
|
||||
zoom: 0.85,
|
||||
center: { lng: 0, lat: 30 },
|
||||
style: MAPBOX_STYLE
|
||||
});
|
||||
|
||||
mapboxMap.addControl(
|
||||
new NavigationControl({ showCompass: false }),
|
||||
'top-left'
|
||||
);
|
||||
|
||||
// set up Popup object
|
||||
popupRef.current = new Popup({
|
||||
closeButton: false,
|
||||
closeOnClick: false
|
||||
});
|
||||
|
||||
// always use the current handler which changes with component state
|
||||
mapboxMap.on('mousemove', (...args) =>
|
||||
updateTooltipStateOnMousemoveRef.current(...args)
|
||||
);
|
||||
mapboxMap.on('mouseout', () => {
|
||||
setTooltipState(null);
|
||||
});
|
||||
|
||||
// only scroll zoom when key is pressed
|
||||
const canvasElement = mapboxMap.getCanvas();
|
||||
canvasElement.addEventListener('wheel', controlScrollZoomOnWheel);
|
||||
|
||||
mapboxMap.on('load', () => {
|
||||
mapboxMap.addSource(CHOROPLETH_POLYGONS_SOURCE_ID, {
|
||||
type: 'geojson',
|
||||
data: GEOJSON_SOURCE
|
||||
});
|
||||
setMap(mapboxMap);
|
||||
});
|
||||
|
||||
// cleanup function called when component unmounts
|
||||
return () => {
|
||||
canvasElement.removeEventListener('wheel', controlScrollZoomOnWheel);
|
||||
};
|
||||
}, [controlScrollZoomOnWheel]);
|
||||
|
||||
// side effect replaces choropleth layer with new one on items changes
|
||||
useEffect(() => {
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find first symbol layer to place new layer in correct order
|
||||
const symbolLayer = (map.getStyle().layers || []).find(
|
||||
({ type }) => type === 'symbol'
|
||||
);
|
||||
|
||||
if (map.getLayer(CHOROPLETH_LAYER_ID)) {
|
||||
map.removeLayer(CHOROPLETH_LAYER_ID);
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stops = items.map(({ key, value }) => [
|
||||
key,
|
||||
getProgressionColor(getValueScale(value))
|
||||
]);
|
||||
|
||||
const fillColor: mapboxgl.FillPaint['fill-color'] = {
|
||||
property: GEOJSON_KEY_PROPERTY,
|
||||
stops,
|
||||
type: 'categorical',
|
||||
default: 'transparent'
|
||||
};
|
||||
|
||||
map.addLayer(
|
||||
{
|
||||
id: CHOROPLETH_LAYER_ID,
|
||||
type: 'fill',
|
||||
source: CHOROPLETH_POLYGONS_SOURCE_ID,
|
||||
layout: {},
|
||||
paint: {
|
||||
'fill-opacity': 0.75,
|
||||
'fill-color': fillColor
|
||||
}
|
||||
},
|
||||
symbolLayer ? symbolLayer.id : undefined
|
||||
);
|
||||
}, [map, items, getValueScale]);
|
||||
|
||||
// side effect to only render the Popup when hovering a region with a matching item
|
||||
useEffect(() => {
|
||||
if (!(popupContainerRef.current && map && popupRef.current)) {
|
||||
return;
|
||||
}
|
||||
if (tooltipState) {
|
||||
popupRef.current.setDOMContent(popupContainerRef.current).addTo(map);
|
||||
if (popupContainerRef.current.parentElement) {
|
||||
popupContainerRef.current.parentElement.style.pointerEvents = 'none';
|
||||
}
|
||||
} else {
|
||||
popupRef.current.remove();
|
||||
}
|
||||
}, [map, tooltipState]);
|
||||
|
||||
// render map container and tooltip in a hidden container
|
||||
return (
|
||||
<div>
|
||||
<div ref={containerRef} style={{ height: 256 }} />
|
||||
<div style={{ display: 'none' }}>
|
||||
<div ref={popupContainerRef}>
|
||||
{tooltipState ? <ChoroplethToolTip {...tooltipState} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { EuiFlexGrid, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useAvgDurationByCountry } from '../../../../../hooks/useAvgDurationByCountry';
|
||||
import { ChoroplethMap } from '../ChoroplethMap';
|
||||
|
||||
export const PageLoadCharts: React.SFC = () => {
|
||||
const { data } = useAvgDurationByCountry();
|
||||
|
||||
return (
|
||||
<EuiFlexGrid columns={1} gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<span>
|
||||
{i18n.translate(
|
||||
'xpack.apm.metrics.pageLoadCharts.avgPageLoadByCountryLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'Avg. page load duration distribution by country'
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
<ChoroplethMap items={data} />
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
);
|
||||
};
|
|
@ -11,7 +11,8 @@ import {
|
|||
EuiIconTip,
|
||||
EuiPanel,
|
||||
EuiText,
|
||||
EuiTitle
|
||||
EuiTitle,
|
||||
EuiSpacer
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Location } from 'history';
|
||||
|
@ -33,6 +34,7 @@ import { LicenseContext } from '../../../../context/LicenseContext';
|
|||
import { TransactionLineChart } from './TransactionLineChart';
|
||||
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
|
||||
import { getTimeFormatter } from '../../../../utils/formatters';
|
||||
import { PageLoadCharts } from './PageLoadCharts';
|
||||
|
||||
interface TransactionChartProps {
|
||||
hasMLJob: boolean;
|
||||
|
@ -53,6 +55,8 @@ const ShiftedEuiText = styled(EuiText)`
|
|||
top: 5px;
|
||||
`;
|
||||
|
||||
const RUM_PAGE_LOAD_TYPE = 'page-load';
|
||||
|
||||
export class TransactionCharts extends Component<TransactionChartProps> {
|
||||
public getMaxY = (responseTimeSeries: TimeSeries[]) => {
|
||||
const coordinates = flatten(
|
||||
|
@ -150,51 +154,59 @@ export class TransactionCharts extends Component<TransactionChartProps> {
|
|||
const formatter = getTimeFormatter(maxY);
|
||||
|
||||
return (
|
||||
<EuiFlexGrid columns={2} gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiPanel>
|
||||
<React.Fragment>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<span>{responseTimeLabel(transactionType)}</span>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<LicenseContext.Consumer>
|
||||
{license =>
|
||||
this.renderMLHeader(
|
||||
idx(license, _ => _.features.ml.is_available)
|
||||
)
|
||||
}
|
||||
</LicenseContext.Consumer>
|
||||
</EuiFlexGroup>
|
||||
<TransactionLineChart
|
||||
series={responseTimeSeries}
|
||||
tickFormatY={this.getResponseTimeTickFormatter(formatter)}
|
||||
formatTooltipValue={this.getResponseTimeTooltipFormatter(
|
||||
formatter
|
||||
)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<>
|
||||
<EuiFlexGrid columns={2} gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiPanel>
|
||||
<React.Fragment>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<span>{responseTimeLabel(transactionType)}</span>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<LicenseContext.Consumer>
|
||||
{license =>
|
||||
this.renderMLHeader(
|
||||
idx(license, _ => _.features.ml.is_available)
|
||||
)
|
||||
}
|
||||
</LicenseContext.Consumer>
|
||||
</EuiFlexGroup>
|
||||
<TransactionLineChart
|
||||
series={responseTimeSeries}
|
||||
tickFormatY={this.getResponseTimeTickFormatter(formatter)}
|
||||
formatTooltipValue={this.getResponseTimeTooltipFormatter(
|
||||
formatter
|
||||
)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem style={{ flexShrink: 1 }}>
|
||||
<EuiPanel>
|
||||
<React.Fragment>
|
||||
<EuiTitle size="xs">
|
||||
<span>{tpmLabel(transactionType)}</span>
|
||||
</EuiTitle>
|
||||
<TransactionLineChart
|
||||
series={tpmSeries}
|
||||
tickFormatY={this.getTPMFormatter}
|
||||
formatTooltipValue={this.getTPMTooltipFormatter}
|
||||
truncateLegends
|
||||
/>
|
||||
</React.Fragment>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
<EuiFlexItem style={{ flexShrink: 1 }}>
|
||||
<EuiPanel>
|
||||
<React.Fragment>
|
||||
<EuiTitle size="xs">
|
||||
<span>{tpmLabel(transactionType)}</span>
|
||||
</EuiTitle>
|
||||
<TransactionLineChart
|
||||
series={tpmSeries}
|
||||
tickFormatY={this.getTPMFormatter}
|
||||
formatTooltipValue={this.getTPMTooltipFormatter}
|
||||
truncateLegends
|
||||
/>
|
||||
</React.Fragment>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
{transactionType === RUM_PAGE_LOAD_TYPE ? (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<PageLoadCharts />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -217,7 +229,7 @@ function tpmLabel(type?: string) {
|
|||
|
||||
function responseTimeLabel(type?: string) {
|
||||
switch (type) {
|
||||
case 'page-load':
|
||||
case RUM_PAGE_LOAD_TYPE:
|
||||
return i18n.translate(
|
||||
'xpack.apm.metrics.transactionChart.pageLoadTimesLabel',
|
||||
{
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { useFetcher } from './useFetcher';
|
||||
import { useUrlParams } from './useUrlParams';
|
||||
import { callApmApi } from '../services/rest/callApmApi';
|
||||
|
||||
export function useAvgDurationByCountry() {
|
||||
const {
|
||||
urlParams: { serviceName, start, end },
|
||||
uiFilters
|
||||
} = useUrlParams();
|
||||
|
||||
const { data = [], error, status } = useFetcher(() => {
|
||||
if (serviceName && start && end) {
|
||||
return callApmApi({
|
||||
pathname:
|
||||
'/api/apm/services/{serviceName}/transaction_groups/avg_duration_by_country',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
uiFilters: JSON.stringify(uiFilters)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [serviceName, start, end, uiFilters]);
|
||||
|
||||
return {
|
||||
data,
|
||||
status,
|
||||
error
|
||||
};
|
||||
}
|
|
@ -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 {
|
||||
CLIENT_GEO_COUNTRY_ISO_CODE,
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_DURATION,
|
||||
TRANSACTION_TYPE
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { PromiseReturnType } from '../../../../typings/common';
|
||||
import { Setup } from '../../helpers/setup_request';
|
||||
import { rangeFilter } from '../../helpers/range_filter';
|
||||
|
||||
export type TransactionAvgDurationByCountryAPIResponse = PromiseReturnType<
|
||||
typeof getTransactionAvgDurationByCountry
|
||||
>;
|
||||
|
||||
export async function getTransactionAvgDurationByCountry({
|
||||
setup,
|
||||
serviceName
|
||||
}: {
|
||||
setup: Setup;
|
||||
serviceName: string;
|
||||
}) {
|
||||
const { uiFiltersES, client, config, start, end } = setup;
|
||||
const params = {
|
||||
index: config.get<string>('apm_oss.transactionIndices'),
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
{ term: { [PROCESSOR_EVENT]: 'transaction' } },
|
||||
{ term: { [TRANSACTION_TYPE]: 'page-load' } },
|
||||
{ exists: { field: CLIENT_GEO_COUNTRY_ISO_CODE } },
|
||||
{ range: rangeFilter(start, end) },
|
||||
...uiFiltersES
|
||||
]
|
||||
}
|
||||
},
|
||||
aggs: {
|
||||
country_code: {
|
||||
terms: {
|
||||
field: CLIENT_GEO_COUNTRY_ISO_CODE,
|
||||
size: 500
|
||||
},
|
||||
aggs: {
|
||||
avg_duration: {
|
||||
avg: { field: TRANSACTION_DURATION }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resp = await client.search(params);
|
||||
|
||||
if (!resp.aggregations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const buckets = resp.aggregations.country_code.buckets;
|
||||
const avgDurationsByCountry = buckets.map(
|
||||
({ key, doc_count, avg_duration: { value } }) => ({
|
||||
key,
|
||||
docCount: doc_count,
|
||||
value: value === null ? 0 : value
|
||||
})
|
||||
);
|
||||
|
||||
return avgDurationsByCountry;
|
||||
}
|
|
@ -29,7 +29,8 @@ import {
|
|||
transactionGroupsBreakdownRoute,
|
||||
transactionGroupsChartsRoute,
|
||||
transactionGroupsDistributionRoute,
|
||||
transactionGroupsRoute
|
||||
transactionGroupsRoute,
|
||||
transactionGroupsAvgDurationByCountry
|
||||
} from './transaction_groups';
|
||||
import {
|
||||
errorGroupsLocalFiltersRoute,
|
||||
|
@ -65,6 +66,7 @@ const createApmApi = () => {
|
|||
.add(transactionGroupsChartsRoute)
|
||||
.add(transactionGroupsDistributionRoute)
|
||||
.add(transactionGroupsRoute)
|
||||
.add(transactionGroupsAvgDurationByCountry)
|
||||
.add(errorGroupsLocalFiltersRoute)
|
||||
.add(metricsLocalFiltersRoute)
|
||||
.add(servicesLocalFiltersRoute)
|
||||
|
|
|
@ -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 { getTransactionAvgDurationByCountry } from '../lib/transactions/avg_duration_by_country';
|
||||
|
||||
export const transactionGroupsRoute = createRoute(() => ({
|
||||
path: '/api/apm/services/{serviceName}/transaction_groups',
|
||||
|
@ -142,3 +143,22 @@ export const transactionGroupsBreakdownRoute = createRoute(() => ({
|
|||
});
|
||||
}
|
||||
}));
|
||||
|
||||
export const transactionGroupsAvgDurationByCountry = createRoute(() => ({
|
||||
path: `/api/apm/services/{serviceName}/transaction_groups/avg_duration_by_country`,
|
||||
params: {
|
||||
path: t.type({
|
||||
serviceName: t.string
|
||||
}),
|
||||
query: t.intersection([uiFiltersRt, rangeRt])
|
||||
},
|
||||
handler: async (req, { path, query }) => {
|
||||
const setup = await setupRequest(req);
|
||||
const { serviceName } = path;
|
||||
|
||||
return getTransactionAvgDurationByCountry({
|
||||
serviceName,
|
||||
setup
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
|
|
@ -68,6 +68,7 @@
|
|||
"@types/json-stable-stringify": "^1.0.32",
|
||||
"@types/jsonwebtoken": "^7.2.7",
|
||||
"@types/lodash": "^3.10.1",
|
||||
"@types/mapbox-gl": "^0.54.1",
|
||||
"@types/memoize-one": "^4.1.0",
|
||||
"@types/mime": "^2.0.1",
|
||||
"@types/mkdirp": "^0.5.2",
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -3335,6 +3335,11 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/geojson@*":
|
||||
version "7946.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad"
|
||||
integrity sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==
|
||||
|
||||
"@types/getopts@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/getopts/-/getopts-2.0.1.tgz#b7e5478fe7571838b45aff736a59ab69b8bcda18"
|
||||
|
@ -3631,6 +3636,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-1.12.0.tgz#acf14294d18e6eba427a5e5d7dfce0f5cd2a9400"
|
||||
integrity sha512-+UzPmwHSEEyv7aGlNkVpuFxp/BirXgl8NnPGCtmyx2KXIzAapoW3IqSVk87/Z3PUk8vEL8Pe1HXEMJbNBOQgtg==
|
||||
|
||||
"@types/mapbox-gl@^0.54.1":
|
||||
version "0.54.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/mapbox-gl/-/mapbox-gl-0.54.3.tgz#6215fbf4dbb555d2ca6ce3be0b1de045eec0f967"
|
||||
integrity sha512-/G06vUcV5ucNB7G9ka6J+VbGtffyUYvfe6A3oae/+csTlHIEHcvyJop3Ic4yeMDxycsQCmBvuwz+owseMuiQ3w==
|
||||
dependencies:
|
||||
"@types/geojson" "*"
|
||||
|
||||
"@types/markdown-it@^0.0.7":
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.7.tgz#75070485a3d8ad11e7deb8287f4430be15bf4d39"
|
||||
|
|
Loading…
Reference in a new issue