mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Replace lens embeddable for most used charts (#155026)
## Summary This PR closes https://github.com/elastic/kibana/issues/152139 This change brings a big performance improvement in Loading of the Charts ### Checklist - [x] Add new endpoint to retrieve filtered data based on URL params - [x] Replace Embeddables with Elastic Charts - [x] Delete existing code for Embeddables - [x] Handle Loaders - [x] Add similar No results found visualisations - [x] Add Cy Tests - [x] Add API Tests ## Demo https://user-images.githubusercontent.com/7416358/232797685-1b009d5d-cd4a-4041-aa33-872647491ced.mov --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
eb9c868779
commit
4b6dbdcd2c
27 changed files with 1398 additions and 574 deletions
20
x-pack/plugins/apm/common/mobile_types.ts
Normal file
20
x-pack/plugins/apm/common/mobile_types.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export enum MobilePropertyType {
|
||||
Device = 'device',
|
||||
NetworkConnectionType = 'netConnectionType',
|
||||
OsVersion = 'osVersion',
|
||||
AppVersion = 'appVersion',
|
||||
}
|
||||
|
||||
export type MobilePropertyNctType = MobilePropertyType.NetworkConnectionType;
|
||||
|
||||
export type MobilePropertyDeviceOsAppVersionType =
|
||||
| MobilePropertyType.Device
|
||||
| MobilePropertyType.OsVersion
|
||||
| MobilePropertyType.AppVersion;
|
|
@ -0,0 +1,392 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
|
||||
export function generateMobileData({ from, to }: { from: number; to: number }) {
|
||||
const range = timerange(from, to);
|
||||
|
||||
const galaxy10 = apm
|
||||
.mobileApp({
|
||||
name: 'synth-android',
|
||||
environment: 'production',
|
||||
agentName: 'android/java',
|
||||
})
|
||||
.mobileDevice({ serviceVersion: '2.3' })
|
||||
.deviceInfo({
|
||||
manufacturer: 'Samsung',
|
||||
modelIdentifier: 'SM-G973F',
|
||||
modelName: 'Galaxy S10',
|
||||
})
|
||||
.osInfo({
|
||||
osType: 'android',
|
||||
osVersion: '10',
|
||||
osFull: 'Android 10, API level 29, BUILD A022MUBU2AUD1',
|
||||
runtimeVersion: '2.1.0',
|
||||
})
|
||||
.setGeoInfo({
|
||||
clientIp: '223.72.43.22',
|
||||
cityName: 'Beijing',
|
||||
continentName: 'Asia',
|
||||
countryIsoCode: 'CN',
|
||||
countryName: 'China',
|
||||
regionIsoCode: 'CN-BJ',
|
||||
regionName: 'Beijing',
|
||||
location: { coordinates: [116.3861, 39.9143], type: 'Point' },
|
||||
})
|
||||
.setNetworkConnection({ type: 'wifi' });
|
||||
|
||||
const galaxy7 = apm
|
||||
.mobileApp({
|
||||
name: 'synth-android',
|
||||
environment: 'production',
|
||||
agentName: 'android/java',
|
||||
})
|
||||
.mobileDevice({ serviceVersion: '1.2' })
|
||||
.deviceInfo({
|
||||
manufacturer: 'Samsung',
|
||||
modelIdentifier: 'SM-G930F',
|
||||
modelName: 'Galaxy S7',
|
||||
})
|
||||
.osInfo({
|
||||
osType: 'android',
|
||||
osVersion: '10',
|
||||
osFull: 'Android 10, API level 29, BUILD A022MUBU2AUD1',
|
||||
runtimeVersion: '2.1.0',
|
||||
})
|
||||
.setGeoInfo({
|
||||
clientIp: '223.72.43.22',
|
||||
cityName: 'Beijing',
|
||||
continentName: 'Asia',
|
||||
countryIsoCode: 'CN',
|
||||
countryName: 'China',
|
||||
regionIsoCode: 'CN-BJ',
|
||||
regionName: 'Beijing',
|
||||
location: { coordinates: [116.3861, 39.9143], type: 'Point' },
|
||||
})
|
||||
.setNetworkConnection({
|
||||
type: 'cell',
|
||||
subType: 'edge',
|
||||
carrierName: 'M1 Limited',
|
||||
carrierMNC: '03',
|
||||
carrierICC: 'SG',
|
||||
carrierMCC: '525',
|
||||
});
|
||||
|
||||
const huaweiP2 = apm
|
||||
.mobileApp({
|
||||
name: 'synth-android',
|
||||
environment: 'production',
|
||||
agentName: 'android/java',
|
||||
})
|
||||
.mobileDevice({ serviceVersion: '1.1' })
|
||||
.deviceInfo({
|
||||
manufacturer: 'Huawei',
|
||||
modelIdentifier: 'HUAWEI P2-0000',
|
||||
modelName: 'HuaweiP2',
|
||||
})
|
||||
.osInfo({
|
||||
osType: 'android',
|
||||
osVersion: '10',
|
||||
osFull: 'Android 10, API level 29, BUILD A022MUBU2AUD1',
|
||||
runtimeVersion: '2.1.0',
|
||||
})
|
||||
.setGeoInfo({
|
||||
clientIp: '20.24.184.101',
|
||||
cityName: 'Singapore',
|
||||
continentName: 'Asia',
|
||||
countryIsoCode: 'SG',
|
||||
countryName: 'Singapore',
|
||||
location: { coordinates: [103.8554, 1.3036], type: 'Point' },
|
||||
})
|
||||
.setNetworkConnection({
|
||||
type: 'cell',
|
||||
subType: 'edge',
|
||||
carrierName: 'Osaka Gas Business Create Co., Ltd.',
|
||||
carrierMNC: '17',
|
||||
carrierICC: 'JP',
|
||||
carrierMCC: '440',
|
||||
});
|
||||
|
||||
const pixel7 = apm
|
||||
.mobileApp({
|
||||
name: 'synth-android',
|
||||
environment: 'production',
|
||||
agentName: 'android/java',
|
||||
})
|
||||
.mobileDevice({ serviceVersion: '2.3' })
|
||||
.deviceInfo({
|
||||
manufacturer: 'Google',
|
||||
modelIdentifier: 'Pixel 7',
|
||||
modelName: 'Pixel 7',
|
||||
})
|
||||
.osInfo({
|
||||
osType: 'android',
|
||||
osVersion: '10',
|
||||
osFull: 'Android 10, API level 29, BUILD A022MUBU2AUD1',
|
||||
runtimeVersion: '2.1.0',
|
||||
})
|
||||
.setGeoInfo({
|
||||
clientIp: '223.72.43.22',
|
||||
cityName: 'Beijing',
|
||||
continentName: 'Asia',
|
||||
countryIsoCode: 'CN',
|
||||
countryName: 'China',
|
||||
regionIsoCode: 'CN-BJ',
|
||||
regionName: 'Beijing',
|
||||
location: { coordinates: [116.3861, 39.9143], type: 'Point' },
|
||||
})
|
||||
.setNetworkConnection({ type: 'wifi' });
|
||||
|
||||
const pixel7Pro = apm
|
||||
.mobileApp({
|
||||
name: 'synth-android',
|
||||
environment: 'production',
|
||||
agentName: 'android/java',
|
||||
})
|
||||
.mobileDevice({ serviceVersion: '2.3' })
|
||||
.deviceInfo({
|
||||
manufacturer: 'Google',
|
||||
modelIdentifier: 'Pixel 7 Pro',
|
||||
modelName: 'Pixel 7 Pro',
|
||||
})
|
||||
.osInfo({
|
||||
osType: 'android',
|
||||
osVersion: '10',
|
||||
osFull: 'Android 10, API level 29, BUILD A022MUBU2AUD1',
|
||||
runtimeVersion: '2.1.0',
|
||||
})
|
||||
.setGeoInfo({
|
||||
clientIp: '223.72.43.22',
|
||||
cityName: 'Beijing',
|
||||
continentName: 'Asia',
|
||||
countryIsoCode: 'CN',
|
||||
countryName: 'China',
|
||||
regionIsoCode: 'CN-BJ',
|
||||
regionName: 'Beijing',
|
||||
location: { coordinates: [116.3861, 39.9143], type: 'Point' },
|
||||
})
|
||||
.setNetworkConnection({ type: 'wifi' });
|
||||
|
||||
const pixel8 = apm
|
||||
.mobileApp({
|
||||
name: 'synth-android',
|
||||
environment: 'production',
|
||||
agentName: 'android/java',
|
||||
})
|
||||
.mobileDevice({ serviceVersion: '2.3' })
|
||||
.deviceInfo({
|
||||
manufacturer: 'Google',
|
||||
modelIdentifier: 'Pixel 8',
|
||||
modelName: 'Pixel 8',
|
||||
})
|
||||
.osInfo({
|
||||
osType: 'android',
|
||||
osVersion: '10',
|
||||
osFull: 'Android 10, API level 29, BUILD A022MUBU2AUD1',
|
||||
runtimeVersion: '2.1.0',
|
||||
})
|
||||
.setGeoInfo({
|
||||
clientIp: '223.72.43.22',
|
||||
cityName: 'Beijing',
|
||||
continentName: 'Asia',
|
||||
countryIsoCode: 'CN',
|
||||
countryName: 'China',
|
||||
regionIsoCode: 'CN-BJ',
|
||||
regionName: 'Beijing',
|
||||
location: { coordinates: [116.3861, 39.9143], type: 'Point' },
|
||||
})
|
||||
.setNetworkConnection({ type: 'wifi' });
|
||||
|
||||
return range.interval('1m').generator((timestamp) => {
|
||||
galaxy10.startNewSession();
|
||||
galaxy7.startNewSession();
|
||||
huaweiP2.startNewSession();
|
||||
pixel7.startNewSession();
|
||||
pixel7Pro.startNewSession();
|
||||
pixel8.startNewSession();
|
||||
return [
|
||||
galaxy10
|
||||
.transaction('Start View - View Appearing', 'Android Activity')
|
||||
.timestamp(timestamp)
|
||||
.duration(500)
|
||||
.success()
|
||||
.children(
|
||||
galaxy10
|
||||
.span({
|
||||
spanName: 'onCreate',
|
||||
spanType: 'app',
|
||||
spanSubtype: 'external',
|
||||
'service.target.type': 'http',
|
||||
'span.destination.service.resource': 'external',
|
||||
})
|
||||
.duration(50)
|
||||
.success()
|
||||
.timestamp(timestamp + 20),
|
||||
galaxy10
|
||||
.httpSpan({
|
||||
spanName: 'GET backend:1234',
|
||||
httpMethod: 'GET',
|
||||
httpUrl: 'https://backend:1234/api/start',
|
||||
})
|
||||
.duration(800)
|
||||
.success()
|
||||
.timestamp(timestamp + 400)
|
||||
),
|
||||
galaxy10
|
||||
.transaction('Second View - View Appearing', 'Android Activity')
|
||||
.timestamp(10000 + timestamp)
|
||||
.duration(300)
|
||||
.failure()
|
||||
.children(
|
||||
galaxy10
|
||||
.httpSpan({
|
||||
spanName: 'GET backend:1234',
|
||||
httpMethod: 'GET',
|
||||
httpUrl: 'https://backend:1234/api/second',
|
||||
})
|
||||
.duration(400)
|
||||
.success()
|
||||
.timestamp(10000 + timestamp + 250)
|
||||
),
|
||||
huaweiP2
|
||||
.transaction('Start View - View Appearing', 'huaweiP2 Activity')
|
||||
.timestamp(timestamp)
|
||||
.duration(20)
|
||||
.success()
|
||||
.children(
|
||||
huaweiP2
|
||||
.span({
|
||||
spanName: 'onCreate',
|
||||
spanType: 'app',
|
||||
spanSubtype: 'external',
|
||||
'service.target.type': 'http',
|
||||
'span.destination.service.resource': 'external',
|
||||
})
|
||||
.duration(50)
|
||||
.success()
|
||||
.timestamp(timestamp + 20),
|
||||
huaweiP2
|
||||
.httpSpan({
|
||||
spanName: 'GET backend:1234',
|
||||
httpMethod: 'GET',
|
||||
httpUrl: 'https://backend:1234/api/start',
|
||||
})
|
||||
.duration(800)
|
||||
.success()
|
||||
.timestamp(timestamp + 400)
|
||||
),
|
||||
galaxy7
|
||||
.transaction('Start View - View Appearing', 'Android Activity')
|
||||
.timestamp(timestamp)
|
||||
.duration(20)
|
||||
.success()
|
||||
.children(
|
||||
galaxy7
|
||||
.span({
|
||||
spanName: 'onCreate',
|
||||
spanType: 'app',
|
||||
spanSubtype: 'external',
|
||||
'service.target.type': 'http',
|
||||
'span.destination.service.resource': 'external',
|
||||
})
|
||||
.duration(50)
|
||||
.success()
|
||||
.timestamp(timestamp + 20),
|
||||
galaxy7
|
||||
.httpSpan({
|
||||
spanName: 'GET backend:1234',
|
||||
httpMethod: 'GET',
|
||||
httpUrl: 'https://backend:1234/api/start',
|
||||
})
|
||||
.duration(800)
|
||||
.success()
|
||||
.timestamp(timestamp + 400)
|
||||
),
|
||||
pixel7
|
||||
.transaction('Start View - View Appearing', 'Android Activity')
|
||||
.timestamp(timestamp)
|
||||
.duration(20)
|
||||
.success()
|
||||
.children(
|
||||
pixel7
|
||||
.span({
|
||||
spanName: 'onCreate',
|
||||
spanType: 'app',
|
||||
spanSubtype: 'external',
|
||||
'service.target.type': 'http',
|
||||
'span.destination.service.resource': 'external',
|
||||
})
|
||||
.duration(50)
|
||||
.success()
|
||||
.timestamp(timestamp + 20),
|
||||
pixel7
|
||||
.httpSpan({
|
||||
spanName: 'GET backend:1234',
|
||||
httpMethod: 'GET',
|
||||
httpUrl: 'https://backend:1234/api/start',
|
||||
})
|
||||
.duration(800)
|
||||
.success()
|
||||
.timestamp(timestamp + 400)
|
||||
),
|
||||
pixel8
|
||||
.transaction('Start View - View Appearing', 'Android Activity')
|
||||
.timestamp(timestamp)
|
||||
.duration(20)
|
||||
.success()
|
||||
.children(
|
||||
pixel8
|
||||
.span({
|
||||
spanName: 'onCreate',
|
||||
spanType: 'app',
|
||||
spanSubtype: 'external',
|
||||
'service.target.type': 'http',
|
||||
'span.destination.service.resource': 'external',
|
||||
})
|
||||
.duration(50)
|
||||
.success()
|
||||
.timestamp(timestamp + 20),
|
||||
pixel8
|
||||
.httpSpan({
|
||||
spanName: 'GET backend:1234',
|
||||
httpMethod: 'GET',
|
||||
httpUrl: 'https://backend:1234/api/start',
|
||||
})
|
||||
.duration(800)
|
||||
.success()
|
||||
.timestamp(timestamp + 400)
|
||||
),
|
||||
pixel7Pro
|
||||
.transaction('Start View - View Appearing', 'Android Activity')
|
||||
.timestamp(timestamp)
|
||||
.duration(20)
|
||||
.success()
|
||||
.children(
|
||||
pixel7Pro
|
||||
.span({
|
||||
spanName: 'onCreate',
|
||||
spanType: 'app',
|
||||
spanSubtype: 'external',
|
||||
'service.target.type': 'http',
|
||||
'span.destination.service.resource': 'external',
|
||||
})
|
||||
.duration(50)
|
||||
.success()
|
||||
.timestamp(timestamp + 20),
|
||||
pixel7Pro
|
||||
.httpSpan({
|
||||
spanName: 'GET backend:1234',
|
||||
httpMethod: 'GET',
|
||||
httpUrl: 'https://backend:1234/api/start',
|
||||
})
|
||||
.duration(800)
|
||||
.success()
|
||||
.timestamp(timestamp + 400)
|
||||
),
|
||||
];
|
||||
});
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import url from 'url';
|
||||
import moment from 'moment/moment';
|
||||
import { synthtrace } from '../../../../synthtrace';
|
||||
import { generateMobileData } from './generate_mobile.data';
|
||||
|
||||
const start = Date.now() - 1000;
|
||||
const end = Date.now();
|
||||
|
||||
const rangeFrom = new Date(start).toISOString();
|
||||
const rangeTo = new Date(end).toISOString();
|
||||
|
||||
const apiRequestsToIntercept = [
|
||||
{
|
||||
endpoint: '/internal/apm/mobile-services/synth-android/most_used_charts?*',
|
||||
aliasName: 'mostUsedChartRequest',
|
||||
},
|
||||
];
|
||||
|
||||
const aliasNames = apiRequestsToIntercept.map(
|
||||
({ aliasName }) => `@${aliasName}`
|
||||
);
|
||||
|
||||
const apmMobileServiceOverview = url.format({
|
||||
pathname: 'app/apm/mobile-services/synth-android',
|
||||
query: {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
},
|
||||
});
|
||||
describe('Mobile Service overview page', () => {
|
||||
before(() => {
|
||||
synthtrace.index(
|
||||
generateMobileData({
|
||||
from: new Date(start).getTime(),
|
||||
to: new Date(end).getTime(),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
synthtrace.clean();
|
||||
});
|
||||
|
||||
describe('Mobile service overview with charts', () => {
|
||||
beforeEach(() => {
|
||||
cy.loginAsEditorUser();
|
||||
cy.visitKibana(apmMobileServiceOverview);
|
||||
apiRequestsToIntercept.map(({ endpoint, aliasName }) => {
|
||||
cy.intercept('GET', endpoint).as(aliasName);
|
||||
});
|
||||
});
|
||||
|
||||
describe('accessing android service page', () => {
|
||||
it('shows the most used charts', () => {
|
||||
cy.wait(aliasNames);
|
||||
|
||||
cy.getByTestSubj('mostUsedChart-device').should('exist');
|
||||
cy.getByTestSubj('mostUsedChart-netConnectionType').should('exist');
|
||||
cy.getByTestSubj('mostUsedChart-osVersion').should('exist');
|
||||
cy.getByTestSubj('mostUsedChart-appVersion').should('exist');
|
||||
});
|
||||
|
||||
it('shows No results found, when no data is present', () => {
|
||||
cy.wait(aliasNames);
|
||||
|
||||
const timeStart = moment(start).subtract(5, 'm').toISOString();
|
||||
const timeEnd = moment(end).subtract(5, 'm').toISOString();
|
||||
|
||||
cy.selectAbsoluteTimeRange(timeStart, timeEnd);
|
||||
|
||||
cy.contains('Update').click();
|
||||
|
||||
cy.wait(aliasNames);
|
||||
|
||||
cy.expectAPIsToHaveBeenCalledWith({
|
||||
apisIntercepted: aliasNames,
|
||||
value: `start=${encodeURIComponent(
|
||||
new Date(timeStart).toISOString()
|
||||
)}&end=${encodeURIComponent(new Date(timeEnd).toISOString())}`,
|
||||
});
|
||||
cy.getByTestSubj('mostUsedNoResultsFound').should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -82,9 +82,6 @@ describe('Service overview page', () => {
|
|||
cy.visitKibana(apmServiceOverview);
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.pathname).to.eq('/app/apm/services/synth-go-1/overview');
|
||||
expect(loc.search).to.eq(
|
||||
`?comparisonEnabled=true&environment=ENVIRONMENT_ALL&kuery=&latencyAggregationType=avg&rangeFrom=${rangeFrom}&rangeTo=${rangeTo}&serviceGroup=&offset=1d&transactionType=request`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -105,9 +102,6 @@ describe('Service overview page', () => {
|
|||
expect(loc.pathname).to.eq(
|
||||
'/app/apm/mobile-services/synth-ios/overview'
|
||||
);
|
||||
expect(loc.search).to.eq(
|
||||
`?comparisonEnabled=true&environment=ENVIRONMENT_ALL&kuery=&latencyAggregationType=avg&rangeFrom=${rangeFrom}&rangeTo=${rangeTo}&serviceGroup=&offset=1d&transactionType=request`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -128,9 +122,6 @@ describe('Service overview page', () => {
|
|||
expect(loc.pathname).to.eq(
|
||||
'/app/apm/mobile-services/synth-android/overview'
|
||||
);
|
||||
expect(loc.search).to.eq(
|
||||
`?comparisonEnabled=true&environment=ENVIRONMENT_ALL&kuery=&latencyAggregationType=avg&rangeFrom=${rangeFrom}&rangeTo=${rangeTo}&serviceGroup=&offset=1d&transactionType=request`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,13 +28,7 @@ import { useTimeRange } from '../../../../hooks/use_time_range';
|
|||
import { useApmRouter } from '../../../../hooks/use_apm_router';
|
||||
import { ServiceOverviewThroughputChart } from '../../service_overview/service_overview_throughput_chart';
|
||||
import { TransactionsTable } from '../../../shared/transactions_table';
|
||||
import {
|
||||
DEVICE_MODEL_IDENTIFIER,
|
||||
HOST_OS_VERSION,
|
||||
NETWORK_CONNECTION_TYPE,
|
||||
SERVICE_VERSION,
|
||||
} from '../../../../../common/es_fields/apm';
|
||||
import { MostUsedChart } from './most_used_chart';
|
||||
import { MostUsedCharts } from './most_used_charts';
|
||||
import { GeoMap } from './geo_map';
|
||||
import { FailedTransactionRateChart } from '../../../shared/charts/failed_transaction_rate_chart';
|
||||
import { ServiceOverviewDependenciesTable } from '../../service_overview/service_overview_dependencies_table';
|
||||
|
@ -66,6 +60,7 @@ export function MobileServiceOverview() {
|
|||
netConnectionType,
|
||||
offset,
|
||||
comparisonEnabled,
|
||||
transactionType,
|
||||
},
|
||||
} = useApmParams('/mobile-services/{serviceName}/overview');
|
||||
|
||||
|
@ -198,73 +193,16 @@ export function MobileServiceOverview() {
|
|||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiFlexGroup direction={rowDirection} gutterSize="s">
|
||||
{/* Device */}
|
||||
<EuiFlexItem>
|
||||
<MostUsedChart
|
||||
title={i18n.translate(
|
||||
'xpack.apm.serviceOverview.mostUsed.device',
|
||||
{
|
||||
defaultMessage: 'Devices',
|
||||
}
|
||||
)}
|
||||
metric={DEVICE_MODEL_IDENTIFIER}
|
||||
start={start}
|
||||
end={end}
|
||||
kuery={kueryWithMobileFilters}
|
||||
filters={embeddableFilters}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{/* NCT */}
|
||||
<EuiFlexItem>
|
||||
<MostUsedChart
|
||||
title={i18n.translate(
|
||||
'xpack.apm.serviceOverview.mostUsed.nct',
|
||||
{
|
||||
defaultMessage: 'Network Connection Type',
|
||||
}
|
||||
)}
|
||||
metric={NETWORK_CONNECTION_TYPE}
|
||||
start={start}
|
||||
end={end}
|
||||
kuery={kueryWithMobileFilters}
|
||||
filters={embeddableFilters}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
{/* OS version */}
|
||||
<EuiFlexItem>
|
||||
<MostUsedChart
|
||||
title={i18n.translate(
|
||||
'xpack.apm.serviceOverview.mostUsed.osVersion',
|
||||
{
|
||||
defaultMessage: 'OS version',
|
||||
}
|
||||
)}
|
||||
metric={HOST_OS_VERSION}
|
||||
start={start}
|
||||
end={end}
|
||||
kuery={kueryWithMobileFilters}
|
||||
filters={embeddableFilters}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{/* App version */}
|
||||
<EuiFlexItem>
|
||||
<MostUsedChart
|
||||
title={i18n.translate(
|
||||
'xpack.apm.serviceOverview.mostUsed.appVersion',
|
||||
{
|
||||
defaultMessage: 'App version',
|
||||
}
|
||||
)}
|
||||
metric={SERVICE_VERSION}
|
||||
start={start}
|
||||
end={end}
|
||||
kuery={kueryWithMobileFilters}
|
||||
filters={embeddableFilters}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<MostUsedCharts
|
||||
kuery={kueryWithMobileFilters}
|
||||
start={start}
|
||||
end={end}
|
||||
environment={environment}
|
||||
transactionType={transactionType}
|
||||
serviceName={serviceName}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Most used chart with Lens gets lens attributes 1`] = `
|
||||
Object {
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "apm_static_index_pattern_id",
|
||||
"name": "indexpattern-datasource-layer-host-os-version",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"state": Object {
|
||||
"datasourceStates": Object {
|
||||
"formBased": Object {
|
||||
"layers": Object {
|
||||
"host-os-version": Object {
|
||||
"columnOrder": Array [
|
||||
"termsColumn",
|
||||
"countColumn",
|
||||
],
|
||||
"columns": Object {
|
||||
"countColumn": Object {
|
||||
"dataType": "number",
|
||||
"isBucketed": false,
|
||||
"label": "Count of records",
|
||||
"operationType": "count",
|
||||
"scale": "ratio",
|
||||
"sourceField": "___records___",
|
||||
},
|
||||
"termsColumn": Object {
|
||||
"dataType": "string",
|
||||
"isBucketed": true,
|
||||
"label": "Top 5 values of host.os.version",
|
||||
"operationType": "terms",
|
||||
"params": Object {
|
||||
"orderBy": Object {
|
||||
"columnId": "countColumn",
|
||||
"type": "column",
|
||||
},
|
||||
"orderDirection": "desc",
|
||||
"size": 5,
|
||||
},
|
||||
"scale": "ordinal",
|
||||
"sourceField": "host.os.version",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"filters": Array [
|
||||
Object {
|
||||
"meta": Object {},
|
||||
"query": Object {
|
||||
"term": Object {
|
||||
"processor.event": "transaction",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"meta": Object {},
|
||||
"query": Object {
|
||||
"term": Object {
|
||||
"service.name": "opbeans-swift",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"meta": Object {},
|
||||
"query": Object {
|
||||
"term": Object {
|
||||
"transaction.type": "request",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"query": Object {
|
||||
"language": "kuery",
|
||||
"query": "",
|
||||
},
|
||||
"visualization": Object {
|
||||
"layers": Array [
|
||||
Object {
|
||||
"categoryDisplay": "default",
|
||||
"layerId": "host-os-version",
|
||||
"layerType": "data",
|
||||
"legendDisplay": "hide",
|
||||
"legendPosition": "bottom",
|
||||
"metrics": Array [
|
||||
"countColumn",
|
||||
],
|
||||
"nestedLegend": false,
|
||||
"numberDisplay": "percent",
|
||||
"primaryGroups": Array [
|
||||
"termsColumn",
|
||||
],
|
||||
},
|
||||
],
|
||||
"shape": "donut",
|
||||
},
|
||||
},
|
||||
"title": "most-used-host-os-version",
|
||||
"visualizationType": "lnsPie",
|
||||
}
|
||||
`;
|
|
@ -1,117 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
CountIndexPatternColumn,
|
||||
TermsIndexPatternColumn,
|
||||
PersistedIndexPatternLayer,
|
||||
PieVisualizationState,
|
||||
TypedLensByValueInput,
|
||||
} from '@kbn/lens-plugin/public';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { APM_STATIC_DATA_VIEW_ID } from '../../../../../../common/data_view_constants';
|
||||
import { MostUsedMetricTypes } from '.';
|
||||
|
||||
const BUCKET_SIZE = 5;
|
||||
|
||||
export function getLensAttributes({
|
||||
metric,
|
||||
filters,
|
||||
kuery = '',
|
||||
}: {
|
||||
metric: MostUsedMetricTypes;
|
||||
filters: Filter[];
|
||||
kuery?: string;
|
||||
}): TypedLensByValueInput['attributes'] {
|
||||
const metricId = metric.replaceAll('.', '-');
|
||||
|
||||
const columnA = 'termsColumn';
|
||||
const columnB = 'countColumn';
|
||||
|
||||
const dataLayer: PersistedIndexPatternLayer = {
|
||||
columnOrder: [columnA, columnB],
|
||||
columns: {
|
||||
[columnA]: {
|
||||
label: i18n.translate(
|
||||
'xpack.apm.serviceOverview.lensFlyout.topValues',
|
||||
{
|
||||
defaultMessage: 'Top {BUCKET_SIZE} values of {metric}',
|
||||
values: {
|
||||
BUCKET_SIZE,
|
||||
metric,
|
||||
},
|
||||
}
|
||||
),
|
||||
dataType: 'string',
|
||||
operationType: 'terms',
|
||||
scale: 'ordinal',
|
||||
sourceField: metric,
|
||||
isBucketed: true,
|
||||
params: {
|
||||
size: BUCKET_SIZE,
|
||||
orderBy: {
|
||||
type: 'column',
|
||||
columnId: columnB,
|
||||
},
|
||||
orderDirection: 'desc',
|
||||
},
|
||||
} as TermsIndexPatternColumn,
|
||||
[columnB]: {
|
||||
label: i18n.translate(
|
||||
'xpack.apm.serviceOverview.lensFlyout.countRecords',
|
||||
{
|
||||
defaultMessage: 'Count of records',
|
||||
}
|
||||
),
|
||||
dataType: 'number',
|
||||
operationType: 'count',
|
||||
scale: 'ratio',
|
||||
isBucketed: false,
|
||||
sourceField: '___records___',
|
||||
} as CountIndexPatternColumn,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
title: `most-used-${metricId}`,
|
||||
visualizationType: 'lnsPie',
|
||||
references: [
|
||||
{
|
||||
type: 'index-pattern',
|
||||
id: APM_STATIC_DATA_VIEW_ID,
|
||||
name: `indexpattern-datasource-layer-${metricId}`,
|
||||
},
|
||||
],
|
||||
state: {
|
||||
visualization: {
|
||||
shape: 'donut',
|
||||
layers: [
|
||||
{
|
||||
layerId: metricId,
|
||||
primaryGroups: [columnA],
|
||||
metrics: [columnB],
|
||||
categoryDisplay: 'default',
|
||||
legendDisplay: 'hide',
|
||||
nestedLegend: false,
|
||||
numberDisplay: 'percent',
|
||||
layerType: 'data',
|
||||
legendPosition: 'bottom',
|
||||
},
|
||||
],
|
||||
} as PieVisualizationState,
|
||||
datasourceStates: {
|
||||
formBased: {
|
||||
layers: {
|
||||
[metricId]: dataLayer,
|
||||
},
|
||||
},
|
||||
},
|
||||
filters,
|
||||
query: { language: 'kuery', query: kuery },
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiTitle, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { ApmPluginStartDeps } from '../../../../../plugin';
|
||||
import { getLensAttributes } from './get_lens_attributes';
|
||||
import {
|
||||
DEVICE_MODEL_IDENTIFIER,
|
||||
HOST_OS_VERSION,
|
||||
NETWORK_CONNECTION_TYPE,
|
||||
SERVICE_VERSION,
|
||||
} from '../../../../../../common/es_fields/apm';
|
||||
|
||||
export type MostUsedMetricTypes =
|
||||
| typeof DEVICE_MODEL_IDENTIFIER
|
||||
| typeof SERVICE_VERSION
|
||||
| typeof HOST_OS_VERSION
|
||||
| typeof NETWORK_CONNECTION_TYPE;
|
||||
|
||||
export function MostUsedChart({
|
||||
title,
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
filters,
|
||||
metric,
|
||||
}: {
|
||||
title: React.ReactNode;
|
||||
start: string;
|
||||
end: string;
|
||||
kuery?: string;
|
||||
filters: Filter[];
|
||||
metric: MostUsedMetricTypes;
|
||||
}) {
|
||||
const { services } = useKibana<ApmPluginStartDeps>();
|
||||
const {
|
||||
lens: { EmbeddableComponent, navigateToPrefilledEditor, canUseEditor },
|
||||
} = services;
|
||||
|
||||
const lensAttributes = useMemo(
|
||||
() =>
|
||||
getLensAttributes({
|
||||
kuery,
|
||||
filters,
|
||||
metric,
|
||||
}),
|
||||
[kuery, filters, metric]
|
||||
);
|
||||
|
||||
const openInLens = useCallback(() => {
|
||||
if (lensAttributes) {
|
||||
navigateToPrefilledEditor(
|
||||
{
|
||||
id: `dataVisualizer-${metric}`,
|
||||
timeRange: {
|
||||
from: start,
|
||||
to: end,
|
||||
},
|
||||
attributes: lensAttributes,
|
||||
},
|
||||
{
|
||||
openInNewTab: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [navigateToPrefilledEditor, lensAttributes, start, end, metric]);
|
||||
|
||||
const getOpenInLensAction = () => {
|
||||
return {
|
||||
id: 'openInLens',
|
||||
type: 'link',
|
||||
getDisplayName() {
|
||||
return i18n.translate('xpack.apm.serviceOverview.openInLens', {
|
||||
defaultMessage: 'Open in Lens',
|
||||
});
|
||||
},
|
||||
getIconType() {
|
||||
return 'visArea';
|
||||
},
|
||||
async isCompatible() {
|
||||
return true;
|
||||
},
|
||||
async execute() {
|
||||
openInLens();
|
||||
return;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPanel hasShadow={false} paddingSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xxxs">
|
||||
<h2>{title}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EmbeddableComponent
|
||||
viewMode={ViewMode.VIEW}
|
||||
id={`most-used-${metric.replaceAll('.', '-')}`}
|
||||
hidePanelTitles
|
||||
withDefaultActions
|
||||
style={{ height: 175 }}
|
||||
attributes={lensAttributes}
|
||||
timeRange={{
|
||||
from: start,
|
||||
to: end,
|
||||
}}
|
||||
{...(canUseEditor() && { extraActions: [getOpenInLensAction()] })}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
import { getLensAttributes } from './get_lens_attributes';
|
||||
import { MostUsedChart, MostUsedMetricTypes } from '.';
|
||||
import { HOST_OS_VERSION } from '../../../../../../common/es_fields/apm';
|
||||
|
||||
const mockEmbeddableComponent = jest.fn();
|
||||
|
||||
function Wrapper({ children }: { children?: ReactNode }) {
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
lens: {
|
||||
EmbeddableComponent: mockEmbeddableComponent.mockReturnValue(
|
||||
<div data-test-subj="lens-mock" />
|
||||
),
|
||||
canUseEditor: jest.fn(() => true),
|
||||
navigateToPrefilledEditor: jest.fn(),
|
||||
},
|
||||
} as Partial<CoreStart>);
|
||||
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<KibanaReactContext.Provider>
|
||||
<MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper>
|
||||
</KibanaReactContext.Provider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
const renderOptions = { wrapper: Wrapper };
|
||||
|
||||
describe('Most used chart with Lens', () => {
|
||||
const props = {
|
||||
metric: HOST_OS_VERSION as MostUsedMetricTypes,
|
||||
filters: [
|
||||
{
|
||||
meta: {},
|
||||
query: {
|
||||
term: {
|
||||
'processor.event': 'transaction',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {},
|
||||
query: {
|
||||
term: {
|
||||
'service.name': 'opbeans-swift',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {},
|
||||
query: {
|
||||
term: {
|
||||
'transaction.type': 'request',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
test('gets lens attributes', () => {
|
||||
expect(getLensAttributes(props)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Renders most used chart with Lens', () => {
|
||||
const start = '2022-10-30T20%3A52%3A47.080Z';
|
||||
const end = '2022-10-31T20%3A52%3A47.080Z';
|
||||
|
||||
render(
|
||||
<MostUsedChart
|
||||
title="Most used os version"
|
||||
start={start}
|
||||
end={end}
|
||||
metric={HOST_OS_VERSION as MostUsedMetricTypes}
|
||||
filters={props.filters}
|
||||
/>,
|
||||
renderOptions
|
||||
);
|
||||
|
||||
expect(mockEmbeddableComponent).toHaveBeenCalledTimes(1);
|
||||
expect(mockEmbeddableComponent.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
timeRange: {
|
||||
from: start,
|
||||
to: end,
|
||||
},
|
||||
attributes: getLensAttributes(props),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexGroupProps,
|
||||
useResizeObserver,
|
||||
} from '@elastic/eui';
|
||||
import { SunburstChart } from './sunburst_chart';
|
||||
import { useBreakpoints } from '../../../../../hooks/use_breakpoints';
|
||||
import { APIReturnType } from '../../../../../services/rest/create_call_apm_api';
|
||||
import { useFetcher } from '../../../../../hooks/use_fetcher';
|
||||
import { MobilePropertyType } from '../../../../../../common/mobile_types';
|
||||
|
||||
type MostUsedCharts =
|
||||
APIReturnType<'GET /internal/apm/mobile-services/{serviceName}/most_used_charts'>['mostUsedCharts'][0];
|
||||
|
||||
const MOST_USED_CHARTS: Array<{ key: MostUsedCharts['key']; label: string }> = [
|
||||
{
|
||||
key: MobilePropertyType.Device,
|
||||
label: i18n.translate('xpack.apm.mobile.charts.device', {
|
||||
defaultMessage: 'Devices',
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: MobilePropertyType.NetworkConnectionType,
|
||||
label: i18n.translate('xpack.apm.mobile.charts.nct', {
|
||||
defaultMessage: 'Network Connection Type',
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: MobilePropertyType.OsVersion,
|
||||
label: i18n.translate('xpack.apm.mobile.charts.osVersion', {
|
||||
defaultMessage: 'OS version',
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: MobilePropertyType.AppVersion,
|
||||
label: i18n.translate('xpack.apm.mobile.charts.appVersion', {
|
||||
defaultMessage: 'App version',
|
||||
}),
|
||||
},
|
||||
];
|
||||
export function MostUsedCharts({
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
environment,
|
||||
transactionType,
|
||||
serviceName,
|
||||
}: {
|
||||
start: string;
|
||||
end: string;
|
||||
kuery: string;
|
||||
environment: string;
|
||||
transactionType?: string;
|
||||
serviceName: string;
|
||||
}) {
|
||||
const { isLarge } = useBreakpoints();
|
||||
const resizeRef = useRef<HTMLDivElement>(null);
|
||||
const dimensions = useResizeObserver(resizeRef.current);
|
||||
const groupDirection: EuiFlexGroupProps['direction'] = isLarge
|
||||
? 'column'
|
||||
: 'row';
|
||||
const { data = { mostUsedCharts: [] }, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/mobile-services/{serviceName}/most_used_charts',
|
||||
{
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
transactionType,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
[start, end, environment, kuery, serviceName, transactionType]
|
||||
);
|
||||
|
||||
const chartWidth = isLarge
|
||||
? dimensions.width
|
||||
: dimensions.width / MOST_USED_CHARTS.length;
|
||||
|
||||
return (
|
||||
<div ref={resizeRef}>
|
||||
<EuiFlexGroup
|
||||
direction={groupDirection}
|
||||
gutterSize="s"
|
||||
justifyContent="spaceBetween"
|
||||
>
|
||||
{MOST_USED_CHARTS.map(({ key, label }) => {
|
||||
const chartData =
|
||||
data?.mostUsedCharts.find((chart) => chart.key === key)?.options ||
|
||||
[];
|
||||
return (
|
||||
<div key={key}>
|
||||
<SunburstChart
|
||||
data={chartData}
|
||||
label={label}
|
||||
chartKey={key}
|
||||
fetchStatus={status}
|
||||
chartWidth={chartWidth}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Chart,
|
||||
Partition,
|
||||
PartitionLayout,
|
||||
Datum,
|
||||
PartialTheme,
|
||||
Settings,
|
||||
} from '@elastic/charts';
|
||||
|
||||
import {
|
||||
EuiFlexItem,
|
||||
euiPaletteColorBlindBehindText,
|
||||
EuiTitle,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
EuiProgress,
|
||||
useEuiFontSize,
|
||||
} from '@elastic/eui';
|
||||
import { IconChartDonut } from '@kbn/chart-icons';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
import { ChartContainer } from '../../../../shared/charts/chart_container';
|
||||
import { FETCH_STATUS } from '../../../../../hooks/use_fetcher';
|
||||
|
||||
const theme: PartialTheme = {
|
||||
chartMargins: { top: 0, left: 0, bottom: 0, right: 0 },
|
||||
partition: {
|
||||
minFontSize: 5,
|
||||
idealFontSizeJump: 1.1,
|
||||
outerSizeRatio: 1,
|
||||
emptySizeRatio: 0.3,
|
||||
circlePadding: 3,
|
||||
},
|
||||
};
|
||||
|
||||
export function SunburstChart({
|
||||
data,
|
||||
label,
|
||||
chartKey,
|
||||
fetchStatus,
|
||||
chartWidth,
|
||||
}: {
|
||||
data?: Array<{ key: string | number; docCount: number }>;
|
||||
label?: string;
|
||||
chartKey: string;
|
||||
fetchStatus: FETCH_STATUS;
|
||||
chartWidth: number;
|
||||
}) {
|
||||
const colors = euiPaletteColorBlindBehindText({ sortBy: 'natural' });
|
||||
const isDataAvailable = data && data.length > 0;
|
||||
const isLoading = fetchStatus === FETCH_STATUS.LOADING;
|
||||
|
||||
// The loader needs to be wrapped inside a div with fixed height to avoid layout shift
|
||||
const ProgressLoader = (
|
||||
<div style={{ height: '5px' }}>
|
||||
{isLoading && (
|
||||
<EuiProgress
|
||||
size="xs"
|
||||
color="accent"
|
||||
style={{ background: 'transparent' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexItem
|
||||
grow={true}
|
||||
key={chartKey}
|
||||
style={{
|
||||
height: '200px',
|
||||
width: chartWidth,
|
||||
}}
|
||||
>
|
||||
<EuiTitle size="xs">
|
||||
<h2
|
||||
css={css`
|
||||
font-size: ${useEuiFontSize('xs').fontSize};
|
||||
`}
|
||||
>
|
||||
{label}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
{ProgressLoader}
|
||||
<EuiSpacer size="m" />
|
||||
<ChartContainer
|
||||
hasData={Boolean(isDataAvailable)}
|
||||
status={fetchStatus}
|
||||
height={200}
|
||||
id={`mostUsedChart-${chartKey}`}
|
||||
>
|
||||
{isDataAvailable ? (
|
||||
<Chart>
|
||||
<Settings theme={theme} />
|
||||
<Partition
|
||||
id={chartKey}
|
||||
data={data}
|
||||
layout={PartitionLayout.sunburst}
|
||||
valueAccessor={(d: Datum) => Number(d.docCount)}
|
||||
valueGetter="percent"
|
||||
layers={[
|
||||
{
|
||||
groupByRollup: (d: Datum) => d.key,
|
||||
nodeLabel: (d: Datum) => d,
|
||||
fillLabel: {
|
||||
fontWeight: 100,
|
||||
maximizeFontSize: true,
|
||||
valueFont: {
|
||||
fontWeight: 900,
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
fillColor: (_, sortIndex) => {
|
||||
return colors[sortIndex];
|
||||
},
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Chart>
|
||||
) : (
|
||||
<NoResultsFound />
|
||||
)}
|
||||
</ChartContainer>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
const noResultsFoundStyle = css({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
export function NoResultsFound() {
|
||||
const noResultsFoundText = i18n.translate(
|
||||
'xpack.apm.mobile.charts.noResultsFound',
|
||||
{
|
||||
defaultMessage: 'No results found',
|
||||
}
|
||||
);
|
||||
return (
|
||||
<div css={noResultsFoundStyle}>
|
||||
<EuiText
|
||||
data-test-subj="mostUsedNoResultsFound"
|
||||
textAlign="center"
|
||||
color="subdued"
|
||||
size="xs"
|
||||
>
|
||||
<EuiIcon type={IconChartDonut} color="subdued" size="l" />
|
||||
<EuiSpacer size="s" />
|
||||
<p>{noResultsFoundText}</p>
|
||||
</EuiText>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -35,7 +35,7 @@ export function usePreferredDataSourceAndBucketSize<
|
|||
?
|
||||
| ApmDocumentType.ServiceTransactionMetric
|
||||
| ApmDocumentType.TransactionMetric
|
||||
| ApmDocumentType.TransactionMetric
|
||||
| ApmDocumentType.TransactionEvent
|
||||
: ApmDocumentType.TransactionMetric | ApmDocumentType.TransactionEvent
|
||||
>;
|
||||
} | null {
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
termQuery,
|
||||
kqlQuery,
|
||||
rangeQuery,
|
||||
} from '@kbn/observability-plugin/server';
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import {
|
||||
DEVICE_MODEL_IDENTIFIER,
|
||||
HOST_OS_VERSION,
|
||||
SERVICE_NAME,
|
||||
SERVICE_VERSION,
|
||||
TRANSACTION_TYPE,
|
||||
} from '../../../../common/es_fields/apm';
|
||||
import { environmentQuery } from '../../../../common/utils/environment_query';
|
||||
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { mergeCountWithOther } from './merge_other_count';
|
||||
import {
|
||||
MobilePropertyType,
|
||||
MobilePropertyDeviceOsAppVersionType,
|
||||
} from '../../../../common/mobile_types';
|
||||
|
||||
export type MobileMostUsedChartResponse = Array<{
|
||||
key: MobilePropertyDeviceOsAppVersionType;
|
||||
options: Array<{
|
||||
key: string | number;
|
||||
docCount: number;
|
||||
}>;
|
||||
}>;
|
||||
export async function getMobileMostUsedCharts({
|
||||
kuery,
|
||||
apmEventClient,
|
||||
serviceName,
|
||||
transactionType,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
kuery: string;
|
||||
apmEventClient: APMEventClient;
|
||||
serviceName: string;
|
||||
transactionType?: string;
|
||||
environment: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}): Promise<MobileMostUsedChartResponse> {
|
||||
const MAX_ITEMS_PER_CHART = 5;
|
||||
const response = await apmEventClient.search('get_mobile_most_used_charts', {
|
||||
apm: {
|
||||
events: [ProcessorEvent.transaction],
|
||||
},
|
||||
body: {
|
||||
track_total_hits: false,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
...termQuery(TRANSACTION_TYPE, transactionType),
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
devices: {
|
||||
terms: {
|
||||
field: DEVICE_MODEL_IDENTIFIER,
|
||||
size: MAX_ITEMS_PER_CHART,
|
||||
},
|
||||
},
|
||||
osVersions: {
|
||||
terms: {
|
||||
field: HOST_OS_VERSION,
|
||||
size: MAX_ITEMS_PER_CHART,
|
||||
},
|
||||
},
|
||||
appVersions: {
|
||||
terms: {
|
||||
field: SERVICE_VERSION,
|
||||
size: MAX_ITEMS_PER_CHART,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
key: MobilePropertyType.Device,
|
||||
options:
|
||||
mergeCountWithOther(
|
||||
response.aggregations?.devices?.buckets,
|
||||
response.aggregations?.devices?.sum_other_doc_count
|
||||
) || [],
|
||||
},
|
||||
{
|
||||
key: MobilePropertyType.OsVersion,
|
||||
options:
|
||||
mergeCountWithOther(
|
||||
response.aggregations?.osVersions?.buckets,
|
||||
response.aggregations?.osVersions?.sum_other_doc_count
|
||||
) || [],
|
||||
},
|
||||
{
|
||||
key: MobilePropertyType.AppVersion,
|
||||
options:
|
||||
mergeCountWithOther(
|
||||
response.aggregations?.appVersions?.buckets,
|
||||
response.aggregations?.appVersions?.sum_other_doc_count
|
||||
) || [],
|
||||
},
|
||||
];
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
termQuery,
|
||||
kqlQuery,
|
||||
rangeQuery,
|
||||
} from '@kbn/observability-plugin/server';
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import {
|
||||
NETWORK_CONNECTION_TYPE,
|
||||
SERVICE_NAME,
|
||||
} from '../../../../common/es_fields/apm';
|
||||
import { environmentQuery } from '../../../../common/utils/environment_query';
|
||||
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { mergeCountWithOther } from './merge_other_count';
|
||||
import {
|
||||
MobilePropertyType,
|
||||
MobilePropertyNctType,
|
||||
} from '../../../../common/mobile_types';
|
||||
|
||||
export interface MobileMostUsedNCTChartResponse {
|
||||
key: MobilePropertyNctType;
|
||||
options: Array<{
|
||||
key: string | number;
|
||||
docCount: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function getMobileMostUsedNCTCharts({
|
||||
kuery,
|
||||
apmEventClient,
|
||||
serviceName,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
kuery: string;
|
||||
apmEventClient: APMEventClient;
|
||||
serviceName: string;
|
||||
transactionType?: string;
|
||||
environment: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}): Promise<MobileMostUsedNCTChartResponse> {
|
||||
const MAX_ITEMS_PER_CHART = 5;
|
||||
const response = await apmEventClient.search(
|
||||
'get_mobile_most_used_nct_charts',
|
||||
{
|
||||
apm: {
|
||||
events: [ProcessorEvent.span],
|
||||
},
|
||||
body: {
|
||||
track_total_hits: false,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
netConnectionTypes: {
|
||||
terms: {
|
||||
field: NETWORK_CONNECTION_TYPE,
|
||||
size: MAX_ITEMS_PER_CHART,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
key: MobilePropertyType.NetworkConnectionType,
|
||||
options:
|
||||
mergeCountWithOther(
|
||||
response.aggregations?.netConnectionTypes?.buckets,
|
||||
response.aggregations?.netConnectionTypes?.sum_other_doc_count
|
||||
) || [],
|
||||
};
|
||||
}
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export function mergeCountWithOther(
|
||||
buckets: Array<{ key: string | number; doc_count: number }> = [],
|
||||
otherCount: number = 0
|
||||
) {
|
||||
const options = buckets.map(({ key, doc_count: docCount }) => ({
|
||||
key,
|
||||
docCount,
|
||||
}));
|
||||
if (otherCount > 0) {
|
||||
options.push({
|
||||
key: 'other',
|
||||
docCount: otherCount,
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
|
@ -26,6 +26,15 @@ import {
|
|||
getMobileTermsByField,
|
||||
MobileTermsByFieldResponse,
|
||||
} from './get_mobile_terms_by_field';
|
||||
import {
|
||||
getMobileMostUsedCharts,
|
||||
MobileMostUsedChartResponse,
|
||||
} from './get_mobile_most_used_charts/get_device_os_app_charts';
|
||||
import {
|
||||
getMobileMostUsedNCTCharts,
|
||||
MobileMostUsedNCTChartResponse,
|
||||
} from './get_mobile_most_used_charts/get_nct_chart';
|
||||
import { MobilePropertyType } from '../../../common/mobile_types';
|
||||
|
||||
const mobileFiltersRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/mobile/filters',
|
||||
|
@ -66,6 +75,60 @@ const mobileFiltersRoute = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const mobileChartsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/most_used_charts',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: t.intersection([
|
||||
kueryRt,
|
||||
rangeRt,
|
||||
environmentRt,
|
||||
t.partial({
|
||||
transactionType: t.string,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
mostUsedCharts: Array<{
|
||||
key: MobilePropertyType;
|
||||
options: MobileMostUsedChartResponse[number]['options'] &
|
||||
MobileMostUsedNCTChartResponse['options'];
|
||||
}>;
|
||||
}> => {
|
||||
const apmEventClient = await getApmEventClient(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const { kuery, environment, start, end, transactionType } = params.query;
|
||||
|
||||
const [deviceOsAndAppVersionChart, nctChart] = await Promise.all([
|
||||
getMobileMostUsedCharts({
|
||||
kuery,
|
||||
environment,
|
||||
transactionType,
|
||||
start,
|
||||
end,
|
||||
serviceName,
|
||||
apmEventClient,
|
||||
}),
|
||||
getMobileMostUsedNCTCharts({
|
||||
kuery,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
serviceName,
|
||||
apmEventClient,
|
||||
}),
|
||||
]);
|
||||
|
||||
return { mostUsedCharts: [...deviceOsAndAppVersionChart, nctChart] };
|
||||
},
|
||||
});
|
||||
|
||||
const mobileStatsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/stats',
|
||||
params: t.type({
|
||||
|
@ -268,6 +331,7 @@ const mobileTermsByFieldRoute = createApmServerRoute({
|
|||
|
||||
export const mobileRouteRepository = {
|
||||
...mobileFiltersRoute,
|
||||
...mobileChartsRoute,
|
||||
...sessionsChartRoute,
|
||||
...httpRequestsChartRoute,
|
||||
...mobileStatsRoute,
|
||||
|
|
|
@ -83,6 +83,7 @@
|
|||
"@kbn/alerts-as-data-utils",
|
||||
"@kbn/exploratory-view-plugin",
|
||||
"@kbn/logging-mocks",
|
||||
"@kbn/chart-icons",
|
||||
"@kbn/observability-shared-plugin",
|
||||
],
|
||||
"exclude": [
|
||||
|
|
|
@ -7165,7 +7165,6 @@
|
|||
"xpack.apm.serviceOveriew.errorsTableOccurrences": "{occurrences} occ.",
|
||||
"xpack.apm.serviceOverview.embeddedMap.error.toastDescription": "L'usine incorporable ayant l'ID \"{embeddableFactoryId}\" est introuvable.",
|
||||
"xpack.apm.serviceOverview.embeddedMap.subtitle": "Carte affichant le nombre total de {currentMap} en fonction du pays et de la région",
|
||||
"xpack.apm.serviceOverview.lensFlyout.topValues": "{BUCKET_SIZE} valeurs les plus élevées de {metric}",
|
||||
"xpack.apm.serviceOverview.mobileCallOutText": "Il s'agit d'un service mobile, qui est actuellement disponible en tant que version d'évaluation technique. Vous pouvez nous aider à améliorer l'expérience en nous envoyant des commentaires. {feedbackLink}.",
|
||||
"xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 environnement} other {# environnements}}",
|
||||
"xpack.apm.settings.agentKeys.apiKeysDisabledErrorDescription": "Contactez votre administrateur système et reportez-vous à {link} pour activer les clés d'API.",
|
||||
|
@ -8095,17 +8094,11 @@
|
|||
"xpack.apm.serviceOverview.latencyColumnDefaultLabel": "Latence",
|
||||
"xpack.apm.serviceOverview.latencyColumnP95Label": "Latence (95e)",
|
||||
"xpack.apm.serviceOverview.latencyColumnP99Label": "Latence (99e)",
|
||||
"xpack.apm.serviceOverview.lensFlyout.countRecords": "Nombre d'enregistrements",
|
||||
"xpack.apm.serviceOverview.loadingText": "Chargement…",
|
||||
"xpack.apm.serviceOverview.mobileCallOutLink": "Donner un retour",
|
||||
"xpack.apm.serviceOverview.mobileCallOutTitle": "APM mobile",
|
||||
"xpack.apm.serviceOverview.mostUsed.appVersion": "Version de l'application",
|
||||
"xpack.apm.serviceOverview.mostUsed.device": "Appareils",
|
||||
"xpack.apm.serviceOverview.mostUsed.nct": "Type de connexion réseau",
|
||||
"xpack.apm.serviceOverview.mostUsed.osVersion": "Version du système d'exploitation",
|
||||
"xpack.apm.serviceOverview.mostUsedTitle": "Le plus utilisé",
|
||||
"xpack.apm.serviceOverview.noResultsText": "Aucune instance trouvée",
|
||||
"xpack.apm.serviceOverview.openInLens": "Ouvrir dans Lens",
|
||||
"xpack.apm.serviceOverview.throughtputChartTitle": "Rendement",
|
||||
"xpack.apm.serviceOverview.tpmHelp": "Le rendement est mesuré en transactions par minute (tpm).",
|
||||
"xpack.apm.serviceOverview.transactionsTableColumnErrorRate": "Taux de transactions ayant échoué",
|
||||
|
|
|
@ -7166,7 +7166,6 @@
|
|||
"xpack.apm.serviceOveriew.errorsTableOccurrences": "{occurrences}件。",
|
||||
"xpack.apm.serviceOverview.embeddedMap.error.toastDescription": "id {embeddableFactoryId}の埋め込み可能ファクトリが見つかりました。",
|
||||
"xpack.apm.serviceOverview.embeddedMap.subtitle": "国と地域別に基づく{currentMap}の総数を示した地図",
|
||||
"xpack.apm.serviceOverview.lensFlyout.topValues": "{metric}の上位の{BUCKET_SIZE}値",
|
||||
"xpack.apm.serviceOverview.mobileCallOutText": "これはモバイルサービスであり、現在はテクニカルプレビューとしてリリースされています。フィードバックを送信して、エクスペリエンスの改善にご協力ください。{feedbackLink}",
|
||||
"xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, other {#個の環境}}",
|
||||
"xpack.apm.settings.agentKeys.apiKeysDisabledErrorDescription": "システム管理者に連絡し、{link}を伝えてAPIキーを有効にしてください。",
|
||||
|
@ -8095,17 +8094,11 @@
|
|||
"xpack.apm.serviceOverview.latencyColumnDefaultLabel": "レイテンシ",
|
||||
"xpack.apm.serviceOverview.latencyColumnP95Label": "レイテンシ(95 番目)",
|
||||
"xpack.apm.serviceOverview.latencyColumnP99Label": "レイテンシ(99 番目)",
|
||||
"xpack.apm.serviceOverview.lensFlyout.countRecords": "レコード数",
|
||||
"xpack.apm.serviceOverview.loadingText": "読み込み中…",
|
||||
"xpack.apm.serviceOverview.mobileCallOutLink": "フィードバックを作成する",
|
||||
"xpack.apm.serviceOverview.mobileCallOutTitle": "モバイルAPM",
|
||||
"xpack.apm.serviceOverview.mostUsed.appVersion": "アプリバージョン",
|
||||
"xpack.apm.serviceOverview.mostUsed.device": "デバイス",
|
||||
"xpack.apm.serviceOverview.mostUsed.nct": "ネットワーク接続タイプ",
|
||||
"xpack.apm.serviceOverview.mostUsed.osVersion": "OSバージョン",
|
||||
"xpack.apm.serviceOverview.mostUsedTitle": "最も使用されている",
|
||||
"xpack.apm.serviceOverview.noResultsText": "インスタンスが見つかりません",
|
||||
"xpack.apm.serviceOverview.openInLens": "Lensで開く",
|
||||
"xpack.apm.serviceOverview.throughtputChartTitle": "スループット",
|
||||
"xpack.apm.serviceOverview.tpmHelp": "スループットは1分あたりのトランザクション数(tpm)で測定されます。",
|
||||
"xpack.apm.serviceOverview.transactionsTableColumnErrorRate": "失敗したトランザクション率",
|
||||
|
|
|
@ -7165,7 +7165,6 @@
|
|||
"xpack.apm.serviceOveriew.errorsTableOccurrences": "{occurrences} 次",
|
||||
"xpack.apm.serviceOverview.embeddedMap.error.toastDescription": "未找到 ID 为“{embeddableFactoryId}”的可嵌入工厂。",
|
||||
"xpack.apm.serviceOverview.embeddedMap.subtitle": "根据国家和区域显示 {currentMap} 总数的地图",
|
||||
"xpack.apm.serviceOverview.lensFlyout.topValues": "{metric} 的排名前 {BUCKET_SIZE} 的值",
|
||||
"xpack.apm.serviceOverview.mobileCallOutText": "这是一项移动服务,它当前以技术预览的形式发布。您可以通过提供反馈来帮助我们改进体验。{feedbackLink}。",
|
||||
"xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 个环境} other {# 个环境}}",
|
||||
"xpack.apm.settings.agentKeys.apiKeysDisabledErrorDescription": "请联系您的系统管理员并参阅{link}以启用 API 密钥。",
|
||||
|
@ -8095,17 +8094,11 @@
|
|||
"xpack.apm.serviceOverview.latencyColumnDefaultLabel": "延迟",
|
||||
"xpack.apm.serviceOverview.latencyColumnP95Label": "延迟(第 95 个)",
|
||||
"xpack.apm.serviceOverview.latencyColumnP99Label": "延迟(第 99 个)",
|
||||
"xpack.apm.serviceOverview.lensFlyout.countRecords": "记录计数",
|
||||
"xpack.apm.serviceOverview.loadingText": "正在加载……",
|
||||
"xpack.apm.serviceOverview.mobileCallOutLink": "反馈",
|
||||
"xpack.apm.serviceOverview.mobileCallOutTitle": "移动 APM",
|
||||
"xpack.apm.serviceOverview.mostUsed.appVersion": "应用版本",
|
||||
"xpack.apm.serviceOverview.mostUsed.device": "设备",
|
||||
"xpack.apm.serviceOverview.mostUsed.nct": "网络连接类型",
|
||||
"xpack.apm.serviceOverview.mostUsed.osVersion": "操作系统版本",
|
||||
"xpack.apm.serviceOverview.mostUsedTitle": "最常用",
|
||||
"xpack.apm.serviceOverview.noResultsText": "未找到实例",
|
||||
"xpack.apm.serviceOverview.openInLens": "在 Lens 中打开",
|
||||
"xpack.apm.serviceOverview.throughtputChartTitle": "吞吐量",
|
||||
"xpack.apm.serviceOverview.tpmHelp": "吞吐量按每分钟事务数 (tpm) 来度量。",
|
||||
"xpack.apm.serviceOverview.transactionsTableColumnErrorRate": "失败事务率",
|
||||
|
|
|
@ -118,6 +118,96 @@ export async function generateMobileData({
|
|||
carrierMCC: '440',
|
||||
});
|
||||
|
||||
const pixel7 = apm
|
||||
.mobileApp({
|
||||
name: 'synth-android',
|
||||
environment: 'production',
|
||||
agentName: 'android/java',
|
||||
})
|
||||
.mobileDevice({ serviceVersion: '2.3' })
|
||||
.deviceInfo({
|
||||
manufacturer: 'Google',
|
||||
modelIdentifier: 'Pixel 7',
|
||||
modelName: 'Pixel 7',
|
||||
})
|
||||
.osInfo({
|
||||
osType: 'android',
|
||||
osVersion: '10',
|
||||
osFull: 'Android 10, API level 29, BUILD A022MUBU2AUD1',
|
||||
runtimeVersion: '2.1.0',
|
||||
})
|
||||
.setGeoInfo({
|
||||
clientIp: '223.72.43.22',
|
||||
cityName: 'Beijing',
|
||||
continentName: 'Asia',
|
||||
countryIsoCode: 'CN',
|
||||
countryName: 'China',
|
||||
regionIsoCode: 'CN-BJ',
|
||||
regionName: 'Beijing',
|
||||
location: { coordinates: [116.3861, 39.9143], type: 'Point' },
|
||||
})
|
||||
.setNetworkConnection({ type: 'wifi' });
|
||||
|
||||
const pixel7Pro = apm
|
||||
.mobileApp({
|
||||
name: 'synth-android',
|
||||
environment: 'production',
|
||||
agentName: 'android/java',
|
||||
})
|
||||
.mobileDevice({ serviceVersion: '2.3' })
|
||||
.deviceInfo({
|
||||
manufacturer: 'Google',
|
||||
modelIdentifier: 'Pixel 7 Pro',
|
||||
modelName: 'Pixel 7 Pro',
|
||||
})
|
||||
.osInfo({
|
||||
osType: 'android',
|
||||
osVersion: '10',
|
||||
osFull: 'Android 10, API level 29, BUILD A022MUBU2AUD1',
|
||||
runtimeVersion: '2.1.0',
|
||||
})
|
||||
.setGeoInfo({
|
||||
clientIp: '223.72.43.22',
|
||||
cityName: 'Beijing',
|
||||
continentName: 'Asia',
|
||||
countryIsoCode: 'CN',
|
||||
countryName: 'China',
|
||||
regionIsoCode: 'CN-BJ',
|
||||
regionName: 'Beijing',
|
||||
location: { coordinates: [116.3861, 39.9143], type: 'Point' },
|
||||
})
|
||||
.setNetworkConnection({ type: 'wifi' });
|
||||
|
||||
const pixel8 = apm
|
||||
.mobileApp({
|
||||
name: 'synth-android',
|
||||
environment: 'production',
|
||||
agentName: 'android/java',
|
||||
})
|
||||
.mobileDevice({ serviceVersion: '2.3' })
|
||||
.deviceInfo({
|
||||
manufacturer: 'Google',
|
||||
modelIdentifier: 'Pixel 8',
|
||||
modelName: 'Pixel 8',
|
||||
})
|
||||
.osInfo({
|
||||
osType: 'android',
|
||||
osVersion: '10',
|
||||
osFull: 'Android 10, API level 29, BUILD A022MUBU2AUD1',
|
||||
runtimeVersion: '2.1.0',
|
||||
})
|
||||
.setGeoInfo({
|
||||
clientIp: '223.72.43.22',
|
||||
cityName: 'Beijing',
|
||||
continentName: 'Asia',
|
||||
countryIsoCode: 'CN',
|
||||
countryName: 'China',
|
||||
regionIsoCode: 'CN-BJ',
|
||||
regionName: 'Beijing',
|
||||
location: { coordinates: [116.3861, 39.9143], type: 'Point' },
|
||||
})
|
||||
.setNetworkConnection({ type: 'wifi' });
|
||||
|
||||
return await synthtraceEsClient.index([
|
||||
timerange(start, end)
|
||||
.interval('5m')
|
||||
|
@ -126,6 +216,9 @@ export async function generateMobileData({
|
|||
galaxy10.startNewSession();
|
||||
galaxy7.startNewSession();
|
||||
huaweiP2.startNewSession();
|
||||
pixel7.startNewSession();
|
||||
pixel7Pro.startNewSession();
|
||||
pixel8.startNewSession();
|
||||
return [
|
||||
galaxy10
|
||||
.transaction('Start View - View Appearing', 'Android Activity')
|
||||
|
@ -224,6 +317,87 @@ export async function generateMobileData({
|
|||
.success()
|
||||
.timestamp(timestamp + 400)
|
||||
),
|
||||
pixel7
|
||||
.transaction('Start View - View Appearing', 'Android Activity')
|
||||
.timestamp(timestamp)
|
||||
.duration(20)
|
||||
.success()
|
||||
.children(
|
||||
pixel7
|
||||
.span({
|
||||
spanName: 'onCreate',
|
||||
spanType: 'app',
|
||||
spanSubtype: 'external',
|
||||
'service.target.type': 'http',
|
||||
'span.destination.service.resource': 'external',
|
||||
})
|
||||
.duration(50)
|
||||
.success()
|
||||
.timestamp(timestamp + 20),
|
||||
pixel7
|
||||
.httpSpan({
|
||||
spanName: 'GET backend:1234',
|
||||
httpMethod: 'GET',
|
||||
httpUrl: 'https://backend:1234/api/start',
|
||||
})
|
||||
.duration(800)
|
||||
.success()
|
||||
.timestamp(timestamp + 400)
|
||||
),
|
||||
pixel8
|
||||
.transaction('Start View - View Appearing', 'Android Activity')
|
||||
.timestamp(timestamp)
|
||||
.duration(20)
|
||||
.success()
|
||||
.children(
|
||||
pixel8
|
||||
.span({
|
||||
spanName: 'onCreate',
|
||||
spanType: 'app',
|
||||
spanSubtype: 'external',
|
||||
'service.target.type': 'http',
|
||||
'span.destination.service.resource': 'external',
|
||||
})
|
||||
.duration(50)
|
||||
.success()
|
||||
.timestamp(timestamp + 20),
|
||||
pixel8
|
||||
.httpSpan({
|
||||
spanName: 'GET backend:1234',
|
||||
httpMethod: 'GET',
|
||||
httpUrl: 'https://backend:1234/api/start',
|
||||
})
|
||||
.duration(800)
|
||||
.success()
|
||||
.timestamp(timestamp + 400)
|
||||
),
|
||||
pixel7Pro
|
||||
.transaction('Start View - View Appearing', 'Android Activity')
|
||||
.timestamp(timestamp)
|
||||
.duration(20)
|
||||
.success()
|
||||
.children(
|
||||
pixel7Pro
|
||||
.span({
|
||||
spanName: 'onCreate',
|
||||
spanType: 'app',
|
||||
spanSubtype: 'external',
|
||||
'service.target.type': 'http',
|
||||
'span.destination.service.resource': 'external',
|
||||
})
|
||||
.duration(50)
|
||||
.success()
|
||||
.timestamp(timestamp + 20),
|
||||
pixel7Pro
|
||||
.httpSpan({
|
||||
spanName: 'GET backend:1234',
|
||||
httpMethod: 'GET',
|
||||
httpUrl: 'https://backend:1234/api/start',
|
||||
})
|
||||
.duration(800)
|
||||
.success()
|
||||
.timestamp(timestamp + 400)
|
||||
),
|
||||
];
|
||||
}),
|
||||
]);
|
||||
|
|
|
@ -92,7 +92,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
response.body.currentPeriod.timeseries.some((item) => item.y === 0 && item.x)
|
||||
).to.eql(true);
|
||||
|
||||
expect(response.body.currentPeriod.timeseries[0].y).to.eql(4);
|
||||
expect(response.body.currentPeriod.timeseries[0].y).to.eql(7);
|
||||
expect(response.body.previousPeriod.timeseries).to.eql([]);
|
||||
});
|
||||
});
|
||||
|
@ -125,7 +125,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(response.status).to.be(200);
|
||||
expect(ntcCell.status).to.be(200);
|
||||
expect(response.body.currentPeriod.timeseries[0].y).to.eql(2);
|
||||
expect(response.body.currentPeriod.timeseries[0].y).to.eql(5);
|
||||
expect(ntcCell.body.currentPeriod.timeseries[0].y).to.eql(2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -122,7 +122,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
kuery: `service.version:"2.3"`,
|
||||
});
|
||||
|
||||
expect(response.currentPeriod.mostSessions.value).to.eql(3);
|
||||
expect(response.currentPeriod.mostSessions.value).to.eql(12);
|
||||
expect(response.currentPeriod.mostRequests.value).to.eql(0);
|
||||
});
|
||||
|
||||
|
@ -132,7 +132,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
kuery: `service.version:"2.3" and service.environment: "production"`,
|
||||
});
|
||||
|
||||
expect(response.currentPeriod.mostSessions.value).to.eql(3);
|
||||
expect(response.currentPeriod.mostSessions.value).to.eql(12);
|
||||
expect(response.currentPeriod.mostRequests.value).to.eql(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { generateMobileData } from './generate_mobile_data';
|
||||
|
||||
type MostUsedCharts =
|
||||
APIReturnType<'GET /internal/apm/mobile-services/{serviceName}/most_used_charts'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const registry = getService('registry');
|
||||
const synthtraceEsClient = getService('synthtraceEsClient');
|
||||
|
||||
const start = new Date('2023-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1;
|
||||
|
||||
async function getMobileMostUsedCharts({
|
||||
environment = ENVIRONMENT_ALL.value,
|
||||
kuery = '',
|
||||
serviceName,
|
||||
transactionType = 'mobile',
|
||||
}: {
|
||||
environment?: string;
|
||||
kuery?: string;
|
||||
serviceName: string;
|
||||
transactionType?: string;
|
||||
}) {
|
||||
return await apmApiClient
|
||||
.readUser({
|
||||
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/most_used_charts',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
environment,
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
kuery,
|
||||
transactionType,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ body }) => body);
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Most used charts when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
describe('when no data', () => {
|
||||
it('handles empty state', async () => {
|
||||
const response: MostUsedCharts = await getMobileMostUsedCharts({ serviceName: 'foo' });
|
||||
expect(response.mostUsedCharts.length).to.eql(4);
|
||||
expect(response.mostUsedCharts.every((chart) => chart.options.length === 0)).to.eql(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
registry.when('Mobile stats', { config: 'basic', archives: [] }, () => {
|
||||
before(async () => {
|
||||
await generateMobileData({
|
||||
synthtraceEsClient,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
describe('when data is loaded', () => {
|
||||
let response: MostUsedCharts;
|
||||
|
||||
before(async () => {
|
||||
response = await getMobileMostUsedCharts({
|
||||
serviceName: 'synth-android',
|
||||
environment: 'production',
|
||||
});
|
||||
});
|
||||
|
||||
it('should get the top 5 and the other option only', () => {
|
||||
const deviceOptions = response.mostUsedCharts.find(
|
||||
(chart) => chart.key === 'device'
|
||||
)?.options;
|
||||
expect(deviceOptions?.length).to.eql(6);
|
||||
expect(deviceOptions?.find((option) => option.key === 'other')).to.not.be(undefined);
|
||||
});
|
||||
|
||||
it('should get network connection type object from span events', () => {
|
||||
const nctOptions = response.mostUsedCharts.find(
|
||||
(chart) => chart.key === 'netConnectionType'
|
||||
)?.options;
|
||||
expect(nctOptions?.length).to.eql(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -78,7 +78,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
true
|
||||
);
|
||||
|
||||
expect(response.body.currentPeriod.timeseries[0].y).to.eql(3);
|
||||
expect(response.body.currentPeriod.timeseries[0].y).to.eql(6);
|
||||
expect(response.body.previousPeriod.timeseries[0].y).to.eql(0);
|
||||
});
|
||||
|
||||
|
@ -90,7 +90,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
true
|
||||
);
|
||||
|
||||
expect(response.body.currentPeriod.timeseries[0].y).to.eql(3);
|
||||
expect(response.body.currentPeriod.timeseries[0].y).to.eql(6);
|
||||
expect(response.body.previousPeriod.timeseries).to.eql([]);
|
||||
});
|
||||
});
|
||||
|
@ -119,7 +119,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
true
|
||||
);
|
||||
|
||||
expect(response.body.currentPeriod.timeseries[0].y).to.eql(3);
|
||||
expect(response.body.currentPeriod.timeseries[0].y).to.eql(6);
|
||||
expect(response.body.previousPeriod.timeseries).to.eql([]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -134,7 +134,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
kuery: `service.version:"2.3" and service.environment: "production"`,
|
||||
});
|
||||
|
||||
expect(response.currentPeriod.sessions.value).to.eql(3);
|
||||
expect(response.currentPeriod.sessions.value).to.eql(12);
|
||||
expect(response.currentPeriod.requests.value).to.eql(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -91,18 +91,12 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
size: 10,
|
||||
});
|
||||
expect(response.terms).to.eql([
|
||||
{
|
||||
label: 'SM-G973F',
|
||||
count: 6,
|
||||
},
|
||||
{
|
||||
label: 'HUAWEI P2-0000',
|
||||
count: 3,
|
||||
},
|
||||
{
|
||||
label: 'SM-G930F',
|
||||
count: 3,
|
||||
},
|
||||
{ label: 'SM-G973F', count: 6 },
|
||||
{ label: 'HUAWEI P2-0000', count: 3 },
|
||||
{ label: 'Pixel 7', count: 3 },
|
||||
{ label: 'Pixel 7 Pro', count: 3 },
|
||||
{ label: 'Pixel 8', count: 3 },
|
||||
{ label: 'SM-G930F', count: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -116,7 +110,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
expect(response.terms).to.eql([
|
||||
{
|
||||
label: '2.3',
|
||||
count: 6,
|
||||
count: 15,
|
||||
},
|
||||
{
|
||||
label: '1.1',
|
||||
|
@ -139,7 +133,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
expect(response.terms).to.eql([
|
||||
{
|
||||
label: '2.3',
|
||||
count: 6,
|
||||
count: 15,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue