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:
Achyut Jhunjhunwala 2023-04-20 14:50:58 +02:00 committed by GitHub
parent eb9c868779
commit 4b6dbdcd2c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1398 additions and 574 deletions

View 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;

View file

@ -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)
),
];
});
}

View file

@ -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');
});
});
});
});

View file

@ -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`
);
});
});
});

View file

@ -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>

View file

@ -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",
}
`;

View file

@ -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 },
},
};
}

View file

@ -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>
);
}

View file

@ -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),
})
);
});
});

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -35,7 +35,7 @@ export function usePreferredDataSourceAndBucketSize<
?
| ApmDocumentType.ServiceTransactionMetric
| ApmDocumentType.TransactionMetric
| ApmDocumentType.TransactionMetric
| ApmDocumentType.TransactionEvent
: ApmDocumentType.TransactionMetric | ApmDocumentType.TransactionEvent
>;
} | null {

View file

@ -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
) || [],
},
];
}

View file

@ -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
) || [],
};
}

View file

@ -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;
}

View file

@ -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,

View file

@ -83,6 +83,7 @@
"@kbn/alerts-as-data-utils",
"@kbn/exploratory-view-plugin",
"@kbn/logging-mocks",
"@kbn/chart-icons",
"@kbn/observability-shared-plugin",
],
"exclude": [

View file

@ -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é",

View file

@ -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": "失敗したトランザクション率",

View file

@ -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": "失败事务率",

View file

@ -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)
),
];
}),
]);

View file

@ -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);
});
});

View file

@ -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);
});
});

View file

@ -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);
});
});
});
}

View file

@ -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([]);
});
});

View file

@ -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);
});
});

View file

@ -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,
},
]);
});