mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[APM] Add table tabs showing summary of metrics (#153044)
Closes https://github.com/elastic/kibana/issues/146877 Notes: - Crash rate is calculated per sessions, the same session ID is kept after a crash, so a session can have more than one crash. - App number of launches is not available - Error rate stays out for now - Http requests, `host.os.version` and `service.version` is only available on transactions, metrics and errors. Not spans for now, there are two issues opened to fix this for the apm mobile agents team - Instead of the View Load (not available), we show Throughput - The filters (+ -) will be added in a follow-up PR Pending: - [x] API tests - [x] e2e tests https://user-images.githubusercontent.com/31922082/234267965-e5e1e411-87c6-40b8-9e94-31d792f9d806.mov --------- Co-authored-by: Yngrid Coello <yngrid.coello@elastic.co>
This commit is contained in:
parent
3864554b36
commit
b6a91f318e
27 changed files with 1826 additions and 64 deletions
|
@ -22,6 +22,7 @@ export type ApmApplicationMetricFields = Partial<{
|
|||
'faas.timeout': number;
|
||||
'faas.coldstart_duration': number;
|
||||
'faas.duration': number;
|
||||
'application.launch.time': number;
|
||||
}>;
|
||||
|
||||
export type ApmUserAgentFields = Partial<{
|
||||
|
@ -88,6 +89,7 @@ export type ApmFields = Fields<{
|
|||
'error.grouping_key': string;
|
||||
'error.grouping_name': string;
|
||||
'error.id': string;
|
||||
'error.type': string;
|
||||
'event.ingested': number;
|
||||
'event.name': string;
|
||||
'event.outcome': string;
|
||||
|
|
|
@ -9,8 +9,10 @@
|
|||
import { Entity } from '../entity';
|
||||
import { Span } from './span';
|
||||
import { Transaction } from './transaction';
|
||||
import { ApmFields, SpanParams, GeoLocation } from './apm_fields';
|
||||
import { ApmFields, SpanParams, GeoLocation, ApmApplicationMetricFields } from './apm_fields';
|
||||
import { generateLongId } from '../utils/generate_id';
|
||||
import { Metricset } from './metricset';
|
||||
import { ApmError } from './apm_error';
|
||||
|
||||
export interface DeviceInfo {
|
||||
manufacturer: string;
|
||||
|
@ -115,6 +117,7 @@ export class MobileDevice extends Entity<ApmFields> {
|
|||
return this;
|
||||
}
|
||||
|
||||
// FIXME synthtrace shouldn't have side-effects like this. We should use an API like .session() which returns a session
|
||||
startNewSession() {
|
||||
this.fields['session.id'] = generateLongId();
|
||||
return this;
|
||||
|
@ -238,4 +241,21 @@ export class MobileDevice extends Entity<ApmFields> {
|
|||
|
||||
return this.span(spanParameters);
|
||||
}
|
||||
|
||||
appMetrics(metrics: ApmApplicationMetricFields) {
|
||||
return new Metricset<ApmFields>({
|
||||
...this.fields,
|
||||
'metricset.name': 'app',
|
||||
...metrics,
|
||||
});
|
||||
}
|
||||
|
||||
crash({ message, groupingName }: { message: string; groupingName?: string }) {
|
||||
return new ApmError({
|
||||
...this.fields,
|
||||
'error.type': 'crash',
|
||||
'error.exception': [{ message, ...{ type: 'crash' } }],
|
||||
'error.grouping_name': groupingName || message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,16 @@ const ENVIRONMENT = getSynthtraceEnvironment(__filename);
|
|||
|
||||
type DeviceMetadata = DeviceInfo & OSInfo;
|
||||
|
||||
const modelIdentifiersWithCrashes = [
|
||||
'SM-G930F',
|
||||
'HUAWEI P2-0000',
|
||||
'Pixel 3a',
|
||||
'LG K10',
|
||||
'iPhone11,8',
|
||||
'Watch6,8',
|
||||
'iPad12,2',
|
||||
];
|
||||
|
||||
const ANDROID_DEVICES: DeviceMetadata[] = [
|
||||
{
|
||||
manufacturer: 'Samsung',
|
||||
|
@ -354,34 +364,40 @@ const scenario: Scenario<ApmFields> = async ({ scenarioOpts, logger }) => {
|
|||
device.startNewSession();
|
||||
const framework =
|
||||
device.fields['device.manufacturer'] === 'Apple' ? 'iOS' : 'Android Activity';
|
||||
const couldCrash = modelIdentifiersWithCrashes.includes(
|
||||
device.fields['device.model.identifier'] ?? ''
|
||||
);
|
||||
const startTx = device
|
||||
.transaction('Start View - View Appearing', framework)
|
||||
.timestamp(timestamp)
|
||||
.duration(500)
|
||||
.success()
|
||||
.children(
|
||||
device
|
||||
.span({
|
||||
spanName: 'onCreate',
|
||||
spanType: 'app',
|
||||
spanSubtype: 'external',
|
||||
'service.target.type': 'http',
|
||||
'span.destination.service.resource': 'external',
|
||||
})
|
||||
.duration(50)
|
||||
.success()
|
||||
.timestamp(timestamp + 20),
|
||||
device
|
||||
.httpSpan({
|
||||
spanName: 'GET backend:1234',
|
||||
httpMethod: 'GET',
|
||||
httpUrl: 'https://backend:1234/api/start',
|
||||
})
|
||||
.duration(800)
|
||||
.failure()
|
||||
.timestamp(timestamp + 400)
|
||||
);
|
||||
return [
|
||||
device
|
||||
.transaction('Start View - View Appearing', framework)
|
||||
.timestamp(timestamp)
|
||||
.duration(500)
|
||||
.success()
|
||||
.children(
|
||||
device
|
||||
.span({
|
||||
spanName: 'onCreate',
|
||||
spanType: 'app',
|
||||
spanSubtype: 'external',
|
||||
'service.target.type': 'http',
|
||||
'span.destination.service.resource': 'external',
|
||||
})
|
||||
.duration(50)
|
||||
.success()
|
||||
.timestamp(timestamp + 20),
|
||||
device
|
||||
.httpSpan({
|
||||
spanName: 'GET backend:1234',
|
||||
httpMethod: 'GET',
|
||||
httpUrl: 'https://backend:1234/api/start',
|
||||
})
|
||||
.duration(800)
|
||||
.failure()
|
||||
.timestamp(timestamp + 400)
|
||||
),
|
||||
couldCrash && index % 2 === 0
|
||||
? startTx.errors(device.crash({ message: 'error' }).timestamp(timestamp))
|
||||
: startTx,
|
||||
device
|
||||
.transaction('Second View - View Appearing', framework)
|
||||
.timestamp(10000 + timestamp)
|
||||
|
@ -418,7 +434,23 @@ const scenario: Scenario<ApmFields> = async ({ scenarioOpts, logger }) => {
|
|||
});
|
||||
};
|
||||
|
||||
return [...androidDevices, ...iOSDevices].map((device) => sessionTransactions(device));
|
||||
const appLaunchMetrics = (device: MobileDevice) => {
|
||||
return clickRate.generator((timestamp, index) =>
|
||||
device
|
||||
.appMetrics({
|
||||
'application.launch.time': 100 * (index + 1),
|
||||
})
|
||||
.timestamp(timestamp)
|
||||
);
|
||||
};
|
||||
|
||||
return [
|
||||
...androidDevices.flatMap((device) => [
|
||||
sessionTransactions(device),
|
||||
appLaunchMetrics(device),
|
||||
]),
|
||||
...iOSDevices.map((device) => sessionTransactions(device)),
|
||||
];
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -13,7 +13,8 @@ type AnyApmDocumentType =
|
|||
| ApmDocumentType.TransactionMetric
|
||||
| ApmDocumentType.TransactionEvent
|
||||
| ApmDocumentType.ServiceDestinationMetric
|
||||
| ApmDocumentType.ServiceSummaryMetric;
|
||||
| ApmDocumentType.ServiceSummaryMetric
|
||||
| ApmDocumentType.ErrorEvent;
|
||||
|
||||
export interface ApmDataSource<
|
||||
TDocumentType extends AnyApmDocumentType = AnyApmDocumentType
|
||||
|
|
|
@ -11,6 +11,7 @@ export enum ApmDocumentType {
|
|||
TransactionEvent = 'transactionEvent',
|
||||
ServiceDestinationMetric = 'serviceDestinationMetric',
|
||||
ServiceSummaryMetric = 'serviceSummaryMetric',
|
||||
ErrorEvent = 'error',
|
||||
}
|
||||
|
||||
export type ApmServiceTransactionDocumentType =
|
||||
|
|
|
@ -90,6 +90,8 @@ exports[`Error ERROR_LOG_MESSAGE 1`] = `undefined`;
|
|||
|
||||
exports[`Error ERROR_PAGE_URL 1`] = `undefined`;
|
||||
|
||||
exports[`Error ERROR_TYPE 1`] = `undefined`;
|
||||
|
||||
exports[`Error EVENT_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error EVENT_OUTCOME 1`] = `undefined`;
|
||||
|
@ -417,6 +419,8 @@ exports[`Span ERROR_LOG_MESSAGE 1`] = `undefined`;
|
|||
|
||||
exports[`Span ERROR_PAGE_URL 1`] = `undefined`;
|
||||
|
||||
exports[`Span ERROR_TYPE 1`] = `undefined`;
|
||||
|
||||
exports[`Span EVENT_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Span EVENT_OUTCOME 1`] = `"unknown"`;
|
||||
|
@ -740,6 +744,8 @@ exports[`Transaction ERROR_LOG_MESSAGE 1`] = `undefined`;
|
|||
|
||||
exports[`Transaction ERROR_PAGE_URL 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction ERROR_TYPE 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction EVENT_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction EVENT_OUTCOME 1`] = `"unknown"`;
|
||||
|
|
|
@ -109,6 +109,7 @@ export const ERROR_EXC_MESSAGE = 'error.exception.message'; // only to be used i
|
|||
export const ERROR_EXC_HANDLED = 'error.exception.handled'; // only to be used in es queries, since error.exception is now an array
|
||||
export const ERROR_EXC_TYPE = 'error.exception.type';
|
||||
export const ERROR_PAGE_URL = 'error.page.url';
|
||||
export const ERROR_TYPE = 'error.type';
|
||||
|
||||
// METRICS
|
||||
export const METRIC_SYSTEM_FREE_MEMORY = 'system.memory.actual.free';
|
||||
|
|
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
* 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 const SERVICE_VERSIONS = ['2.3', '1.2', '1.1'];
|
||||
|
||||
export function generateMobileData({ from, to }: { from: number; to: number }) {
|
||||
const galaxy10 = apm
|
||||
.mobileApp({
|
||||
name: 'synth-android',
|
||||
environment: 'production',
|
||||
agentName: 'android/java',
|
||||
})
|
||||
.mobileDevice({ serviceVersion: SERVICE_VERSIONS[0] })
|
||||
.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: SERVICE_VERSIONS[1] })
|
||||
.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: SERVICE_VERSIONS[2] })
|
||||
.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',
|
||||
});
|
||||
|
||||
return timerange(from, to)
|
||||
.interval('5m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
galaxy10.startNewSession();
|
||||
galaxy7.startNewSession();
|
||||
huaweiP2.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)
|
||||
),
|
||||
];
|
||||
});
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { synthtrace } from '../../../../synthtrace';
|
||||
import { generateMobileData } from './generate_data';
|
||||
|
||||
const start = '2021-10-10T00:00:00.000Z';
|
||||
const end = '2021-10-10T00:15:00.000Z';
|
||||
|
||||
const mobileTransactionsPageHref = url.format({
|
||||
pathname: '/app/apm/mobile-services/synth-android/transactions',
|
||||
query: {
|
||||
rangeFrom: start,
|
||||
rangeTo: end,
|
||||
},
|
||||
});
|
||||
|
||||
describe('Mobile transactions page', () => {
|
||||
beforeEach(() => {
|
||||
cy.loginAsViewerUser();
|
||||
});
|
||||
|
||||
describe('when data is loaded', () => {
|
||||
before(() => {
|
||||
synthtrace.index(
|
||||
generateMobileData({
|
||||
from: new Date(start).getTime(),
|
||||
to: new Date(end).getTime(),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
synthtrace.clean();
|
||||
});
|
||||
|
||||
describe('when click on tab shows correct table', () => {
|
||||
it('shows version tab', () => {
|
||||
cy.visitKibana(mobileTransactionsPageHref);
|
||||
cy.getByTestSubj('apmAppVersionTab')
|
||||
.click()
|
||||
.should('have.attr', 'aria-selected', 'true');
|
||||
cy.url().should('include', 'mobileSelectedTab=app_version_tab');
|
||||
});
|
||||
|
||||
it('shows OS version tab', () => {
|
||||
cy.visitKibana(mobileTransactionsPageHref);
|
||||
cy.getByTestSubj('apmOsVersionTab')
|
||||
.click()
|
||||
.should('have.attr', 'aria-selected', 'true');
|
||||
cy.url().should('include', 'mobileSelectedTab=os_version_tab');
|
||||
});
|
||||
|
||||
it('shows devices tab', () => {
|
||||
cy.visitKibana(mobileTransactionsPageHref);
|
||||
cy.getByTestSubj('apmDevicesTab')
|
||||
.click()
|
||||
.should('have.attr', 'aria-selected', 'true');
|
||||
cy.url().should('include', 'mobileSelectedTab=devices_tab');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -16,11 +16,11 @@ import { useHistory } from 'react-router-dom';
|
|||
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||
import { TransactionsTable } from '../../../shared/transactions_table';
|
||||
import { replace } from '../../../shared/links/url_helpers';
|
||||
import { getKueryWithMobileFilters } from '../../../../../common/utils/get_kuery_with_mobile_filters';
|
||||
import { MobileTransactionCharts } from './transaction_charts';
|
||||
import { MobileTreemap } from '../charts/mobile_treemap';
|
||||
import { TransactionOverviewTabs } from './transaction_overview_tabs';
|
||||
|
||||
export function MobileTransactionOverview() {
|
||||
const {
|
||||
|
@ -37,6 +37,7 @@ export function MobileTransactionOverview() {
|
|||
kuery,
|
||||
offset,
|
||||
comparisonEnabled,
|
||||
mobileSelectedTab,
|
||||
},
|
||||
} = useApmParams('/mobile-services/{serviceName}/transactions');
|
||||
|
||||
|
@ -88,15 +89,14 @@ export function MobileTransactionOverview() {
|
|||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiPanel hasBorder={true}>
|
||||
<TransactionsTable
|
||||
hideViewTransactionsLink
|
||||
numberOfTransactionsPerPage={25}
|
||||
showMaxTransactionGroupsExceededWarning
|
||||
<TransactionOverviewTabs
|
||||
environment={environment}
|
||||
kuery={kueryWithMobileFilters}
|
||||
start={start}
|
||||
end={end}
|
||||
saveTableOptionsToUrl
|
||||
comparisonEnabled={comparisonEnabled}
|
||||
offset={offset}
|
||||
mobileSelectedTab={mobileSelectedTab}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { TabContentProps } from '.';
|
||||
import { isPending } from '../../../../../hooks/use_fetcher';
|
||||
import { StatsList } from './stats_list';
|
||||
import { SERVICE_VERSION } from '../../../../../../common/es_fields/apm';
|
||||
import { useMobileStatisticsFetcher } from './use_mobile_statistics_fetcher';
|
||||
|
||||
function AppVersionTab({
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
}: TabContentProps) {
|
||||
const {
|
||||
mainStatistics,
|
||||
mainStatisticsStatus,
|
||||
detailedStatistics,
|
||||
detailedStatisticsStatus,
|
||||
} = useMobileStatisticsFetcher({
|
||||
field: SERVICE_VERSION,
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
});
|
||||
|
||||
return (
|
||||
<StatsList
|
||||
isLoading={isPending(mainStatisticsStatus)}
|
||||
mainStatistics={mainStatistics}
|
||||
detailedStatisticsLoading={isPending(detailedStatisticsStatus)}
|
||||
detailedStatistics={detailedStatistics}
|
||||
comparisonEnabled={comparisonEnabled}
|
||||
offset={offset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const appVersionTab = {
|
||||
dataTestSubj: 'apmAppVersionTab',
|
||||
key: 'app_version_tab',
|
||||
label: i18n.translate(
|
||||
'xpack.apm.mobile.transactions.overview.tabs.app.version',
|
||||
{
|
||||
defaultMessage: 'App version',
|
||||
}
|
||||
),
|
||||
component: AppVersionTab,
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { TabContentProps } from '.';
|
||||
import { isPending } from '../../../../../hooks/use_fetcher';
|
||||
import { StatsList } from './stats_list';
|
||||
import { useMobileStatisticsFetcher } from './use_mobile_statistics_fetcher';
|
||||
import { DEVICE_MODEL_IDENTIFIER } from '../../../../../../common/es_fields/apm';
|
||||
|
||||
function DevicesTab({
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
}: TabContentProps) {
|
||||
const {
|
||||
mainStatistics,
|
||||
mainStatisticsStatus,
|
||||
detailedStatistics,
|
||||
detailedStatisticsStatus,
|
||||
} = useMobileStatisticsFetcher({
|
||||
field: DEVICE_MODEL_IDENTIFIER,
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
});
|
||||
|
||||
return (
|
||||
<StatsList
|
||||
isLoading={isPending(mainStatisticsStatus)}
|
||||
mainStatistics={mainStatistics}
|
||||
detailedStatisticsLoading={isPending(detailedStatisticsStatus)}
|
||||
detailedStatistics={detailedStatistics}
|
||||
comparisonEnabled={comparisonEnabled}
|
||||
offset={offset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const devicesTab = {
|
||||
dataTestSubj: 'apmDevicesTab',
|
||||
key: 'devices_tab',
|
||||
label: i18n.translate('xpack.apm.mobile.transactions.overview.tabs.devices', {
|
||||
defaultMessage: 'Devices',
|
||||
}),
|
||||
component: DevicesTab,
|
||||
};
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 { useHistory } from 'react-router-dom';
|
||||
import { EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui';
|
||||
import { push } from '../../../../shared/links/url_helpers';
|
||||
import { transactionsTab } from './transactions_tab';
|
||||
import { osVersionTab } from './os_version_tab';
|
||||
import { appVersionTab } from './app_version_tab';
|
||||
import { devicesTab } from './devices_tab';
|
||||
|
||||
export interface TabContentProps {
|
||||
agentName?: string;
|
||||
environment: string;
|
||||
start: string;
|
||||
end: string;
|
||||
kuery: string;
|
||||
comparisonEnabled: boolean;
|
||||
offset?: string;
|
||||
mobileSelectedTab?: string;
|
||||
}
|
||||
|
||||
const tabs = [transactionsTab, appVersionTab, osVersionTab, devicesTab];
|
||||
|
||||
export function TransactionOverviewTabs({
|
||||
agentName,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
mobileSelectedTab,
|
||||
}: TabContentProps) {
|
||||
const history = useHistory();
|
||||
|
||||
const { component: TabContent } =
|
||||
tabs.find((tab) => tab.key === mobileSelectedTab) ?? transactionsTab;
|
||||
return (
|
||||
<>
|
||||
<EuiTabs>
|
||||
{tabs.map(({ dataTestSubj, key, label }) => (
|
||||
<EuiTab
|
||||
data-test-subj={dataTestSubj}
|
||||
key={key}
|
||||
isSelected={key === mobileSelectedTab}
|
||||
onClick={() => {
|
||||
push(history, {
|
||||
query: {
|
||||
mobileSelectedTab: key,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</EuiTab>
|
||||
))}
|
||||
</EuiTabs>
|
||||
<EuiSpacer size="m" />
|
||||
<TabContent
|
||||
{...{
|
||||
agentName,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { TabContentProps } from '.';
|
||||
import { isPending } from '../../../../../hooks/use_fetcher';
|
||||
import { StatsList } from './stats_list';
|
||||
import { useMobileStatisticsFetcher } from './use_mobile_statistics_fetcher';
|
||||
import { HOST_OS_VERSION } from '../../../../../../common/es_fields/apm';
|
||||
|
||||
function OSVersionTab({
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
}: TabContentProps) {
|
||||
const {
|
||||
mainStatistics,
|
||||
mainStatisticsStatus,
|
||||
detailedStatistics,
|
||||
detailedStatisticsStatus,
|
||||
} = useMobileStatisticsFetcher({
|
||||
field: HOST_OS_VERSION,
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
});
|
||||
|
||||
return (
|
||||
<StatsList
|
||||
isLoading={isPending(mainStatisticsStatus)}
|
||||
mainStatistics={mainStatistics}
|
||||
detailedStatisticsLoading={isPending(detailedStatisticsStatus)}
|
||||
detailedStatistics={detailedStatistics}
|
||||
comparisonEnabled={comparisonEnabled}
|
||||
offset={offset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const osVersionTab = {
|
||||
dataTestSubj: 'apmOsVersionTab',
|
||||
key: 'os_version_tab',
|
||||
label: i18n.translate(
|
||||
'xpack.apm.mobile.transactions.overview.tabs.os.version',
|
||||
{
|
||||
defaultMessage: 'OS version',
|
||||
}
|
||||
),
|
||||
component: OSVersionTab,
|
||||
};
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* 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 { RIGHT_ALIGNMENT, EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ValuesType } from 'utility-types';
|
||||
import { APIReturnType } from '../../../../../../services/rest/create_call_apm_api';
|
||||
import {
|
||||
ChartType,
|
||||
getTimeSeriesColor,
|
||||
} from '../../../../../shared/charts/helper/get_timeseries_color';
|
||||
import { SparkPlot } from '../../../../../shared/charts/spark_plot';
|
||||
import { isTimeComparison } from '../../../../../shared/time_comparison/get_comparison_options';
|
||||
import {
|
||||
asMillisecondDuration,
|
||||
asPercent,
|
||||
asTransactionRate,
|
||||
} from '../../../../../../../common/utils/formatters';
|
||||
import { ITableColumn } from '../../../../../shared/managed_table';
|
||||
|
||||
type MobileMainStatisticsByField =
|
||||
APIReturnType<'GET /internal/apm/mobile-services/{serviceName}/main_statistics'>;
|
||||
|
||||
type MobileMainStatisticsByFieldItem = ValuesType<
|
||||
MobileMainStatisticsByField['mainStatistics']
|
||||
>;
|
||||
|
||||
type MobileDetailedStatisticsByField =
|
||||
APIReturnType<'GET /internal/apm/mobile-services/{serviceName}/detailed_statistics'>;
|
||||
|
||||
export function getColumns({
|
||||
agentName,
|
||||
detailedStatisticsLoading,
|
||||
detailedStatistics,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
}: {
|
||||
agentName?: string;
|
||||
detailedStatisticsLoading: boolean;
|
||||
detailedStatistics: MobileDetailedStatisticsByField;
|
||||
comparisonEnabled?: boolean;
|
||||
offset?: string;
|
||||
}): Array<ITableColumn<MobileMainStatisticsByFieldItem>> {
|
||||
return [
|
||||
// version/device
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.mobile.transactions.overview.table.nameColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Name',
|
||||
}
|
||||
),
|
||||
},
|
||||
// latency
|
||||
{
|
||||
field: 'latency',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.mobile.transactions.overview.table.latencyColumnAvgLabel',
|
||||
{
|
||||
defaultMessage: 'Latency (avg.)',
|
||||
}
|
||||
),
|
||||
align: RIGHT_ALIGNMENT,
|
||||
render: (_, { latency, name }) => {
|
||||
const currentPeriodTimeseries =
|
||||
detailedStatistics?.currentPeriod?.[name]?.latency;
|
||||
const previousPeriodTimeseries =
|
||||
detailedStatistics?.previousPeriod?.[name]?.latency;
|
||||
|
||||
const { currentPeriodColor, previousPeriodColor } = getTimeSeriesColor(
|
||||
ChartType.LATENCY_AVG
|
||||
);
|
||||
|
||||
return (
|
||||
<SparkPlot
|
||||
color={currentPeriodColor}
|
||||
isLoading={detailedStatisticsLoading}
|
||||
series={currentPeriodTimeseries}
|
||||
valueLabel={asMillisecondDuration(latency)}
|
||||
comparisonSeries={
|
||||
comparisonEnabled && isTimeComparison(offset)
|
||||
? previousPeriodTimeseries
|
||||
: undefined
|
||||
}
|
||||
comparisonSeriesColor={previousPeriodColor}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
// throughput
|
||||
{
|
||||
field: 'throughput',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.mobile.transactions.overview.table.throughputColumnAvgLabel',
|
||||
{ defaultMessage: 'Throughput' }
|
||||
),
|
||||
align: RIGHT_ALIGNMENT,
|
||||
render: (_, { throughput, name }) => {
|
||||
const currentPeriodTimeseries =
|
||||
detailedStatistics?.currentPeriod?.[name]?.throughput;
|
||||
const previousPeriodTimeseries =
|
||||
detailedStatistics?.previousPeriod?.[name]?.throughput;
|
||||
|
||||
const { currentPeriodColor, previousPeriodColor } = getTimeSeriesColor(
|
||||
ChartType.THROUGHPUT
|
||||
);
|
||||
|
||||
return (
|
||||
<SparkPlot
|
||||
color={currentPeriodColor}
|
||||
isLoading={detailedStatisticsLoading}
|
||||
series={currentPeriodTimeseries}
|
||||
valueLabel={asTransactionRate(throughput)}
|
||||
comparisonSeries={
|
||||
comparisonEnabled && isTimeComparison(offset)
|
||||
? previousPeriodTimeseries
|
||||
: undefined
|
||||
}
|
||||
comparisonSeriesColor={previousPeriodColor}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
// crash rate
|
||||
{
|
||||
field: 'crashRate',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.mobile.transactions.overview.table.crashRateColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Crash rate',
|
||||
}
|
||||
),
|
||||
align: RIGHT_ALIGNMENT,
|
||||
render: (_, { crashRate }) => {
|
||||
return (
|
||||
<EuiText size="s" textAlign="right">
|
||||
{asPercent(crashRate, 1)}
|
||||
</EuiText>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ManagedTable } from '../../../../../shared/managed_table';
|
||||
import { APIReturnType } from '../../../../../../services/rest/create_call_apm_api';
|
||||
import { getColumns } from './get_columns';
|
||||
|
||||
type MobileMainStatisticsByField =
|
||||
APIReturnType<'GET /internal/apm/mobile-services/{serviceName}/main_statistics'>['mainStatistics'];
|
||||
|
||||
type MobileDetailedStatisticsByField =
|
||||
APIReturnType<'GET /internal/apm/mobile-services/{serviceName}/detailed_statistics'>;
|
||||
|
||||
interface Props {
|
||||
isLoading: boolean;
|
||||
mainStatistics: MobileMainStatisticsByField;
|
||||
detailedStatisticsLoading: boolean;
|
||||
detailedStatistics: MobileDetailedStatisticsByField;
|
||||
comparisonEnabled?: boolean;
|
||||
offset?: string;
|
||||
}
|
||||
export function StatsList({
|
||||
isLoading,
|
||||
mainStatistics,
|
||||
detailedStatisticsLoading,
|
||||
detailedStatistics,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
}: Props) {
|
||||
const columns = useMemo(() => {
|
||||
return getColumns({
|
||||
detailedStatisticsLoading,
|
||||
detailedStatistics,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
});
|
||||
}, [
|
||||
detailedStatisticsLoading,
|
||||
detailedStatistics,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
]);
|
||||
return (
|
||||
<ManagedTable
|
||||
noItemsMessage={
|
||||
isLoading
|
||||
? i18n.translate('xpack.apm.mobile.stats.table.loading', {
|
||||
defaultMessage: 'Loading...',
|
||||
})
|
||||
: i18n.translate('xpack.apm.mobile.stats.table.noDataMessage', {
|
||||
defaultMessage: 'No data found',
|
||||
})
|
||||
}
|
||||
items={mainStatistics}
|
||||
columns={columns}
|
||||
sortItems={false}
|
||||
initialPageSize={25}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { TabContentProps } from '.';
|
||||
import { TransactionsTable } from '../../../../shared/transactions_table';
|
||||
|
||||
function TransactionsTab({ environment, kuery, start, end }: TabContentProps) {
|
||||
return (
|
||||
<TransactionsTable
|
||||
hideTitle
|
||||
hideViewTransactionsLink
|
||||
numberOfTransactionsPerPage={25}
|
||||
showMaxTransactionGroupsExceededWarning
|
||||
environment={environment}
|
||||
kuery={kuery}
|
||||
start={start}
|
||||
end={end}
|
||||
saveTableOptionsToUrl
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const transactionsTab = {
|
||||
dataTestSubj: 'apmTransactionsTab',
|
||||
key: 'transactions',
|
||||
label: i18n.translate(
|
||||
'xpack.apm.mobile.transactions.overview.tabs.transactions',
|
||||
{
|
||||
defaultMessage: 'Transactions',
|
||||
}
|
||||
),
|
||||
component: TransactionsTab,
|
||||
};
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 { v4 as uuidv4 } from 'uuid';
|
||||
import { useApmServiceContext } from '../../../../../context/apm_service/use_apm_service_context';
|
||||
import { useFetcher } from '../../../../../hooks/use_fetcher';
|
||||
import { isTimeComparison } from '../../../../shared/time_comparison/get_comparison_options';
|
||||
|
||||
const INITIAL_STATE_MAIN_STATISTICS = {
|
||||
mainStatistics: [],
|
||||
requestId: undefined,
|
||||
totalItems: 0,
|
||||
};
|
||||
|
||||
const INITIAL_STATE_DETAILED_STATISTICS = {
|
||||
currentPeriod: {},
|
||||
previousPeriod: {},
|
||||
};
|
||||
|
||||
interface Props {
|
||||
field: string;
|
||||
environment: string;
|
||||
start: string;
|
||||
end: string;
|
||||
kuery: string;
|
||||
comparisonEnabled: boolean;
|
||||
offset?: string;
|
||||
}
|
||||
|
||||
export function useMobileStatisticsFetcher({
|
||||
field,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
}: Props) {
|
||||
const { serviceName } = useApmServiceContext();
|
||||
|
||||
const { data = INITIAL_STATE_MAIN_STATISTICS, status: mainStatisticsStatus } =
|
||||
useFetcher(
|
||||
(callApmApi) => {
|
||||
if (start && end) {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/mobile-services/{serviceName}/main_statistics',
|
||||
{
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
field,
|
||||
},
|
||||
},
|
||||
}
|
||||
).then((response) => {
|
||||
return {
|
||||
// Everytime the main statistics is refetched, updates the requestId making the comparison API to be refetched.
|
||||
requestId: uuidv4(),
|
||||
mainStatistics: response.mainStatistics,
|
||||
totalItems: response.mainStatistics.length,
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
[environment, start, end, kuery, serviceName, field]
|
||||
);
|
||||
|
||||
const { mainStatistics, requestId, totalItems } = data;
|
||||
|
||||
const {
|
||||
data: detailedStatistics = INITIAL_STATE_DETAILED_STATISTICS,
|
||||
status: detailedStatisticsStatus,
|
||||
} = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (totalItems && start && end) {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/mobile-services/{serviceName}/detailed_statistics',
|
||||
{
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
field,
|
||||
fieldValues: JSON.stringify(
|
||||
data?.mainStatistics.map(({ name }) => name).sort()
|
||||
),
|
||||
offset:
|
||||
comparisonEnabled && isTimeComparison(offset)
|
||||
? offset
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
// only fetches agg results when requestId changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[requestId],
|
||||
{ preservePreviousData: false }
|
||||
);
|
||||
|
||||
return {
|
||||
mainStatistics,
|
||||
mainStatisticsStatus,
|
||||
detailedStatistics,
|
||||
detailedStatisticsStatus,
|
||||
};
|
||||
}
|
|
@ -146,6 +146,7 @@ export const mobileServiceDetail = {
|
|||
osVersion: t.string,
|
||||
appVersion: t.string,
|
||||
netConnectionType: t.string,
|
||||
mobileSelectedTab: t.string,
|
||||
}),
|
||||
}),
|
||||
children: {
|
||||
|
|
|
@ -65,6 +65,7 @@ const DEFAULT_SORT = {
|
|||
};
|
||||
|
||||
interface Props {
|
||||
hideTitle?: boolean;
|
||||
hideViewTransactionsLink?: boolean;
|
||||
isSingleColumn?: boolean;
|
||||
numberOfTransactionsPerPage?: number;
|
||||
|
@ -81,6 +82,7 @@ interface Props {
|
|||
export function TransactionsTable({
|
||||
fixedHeight = false,
|
||||
hideViewTransactionsLink = false,
|
||||
hideTitle = false,
|
||||
isSingleColumn = true,
|
||||
numberOfTransactionsPerPage = 5,
|
||||
showPerPageOptions = true,
|
||||
|
@ -294,32 +296,35 @@ export function TransactionsTable({
|
|||
gutterSize="s"
|
||||
data-test-subj="transactionsGroupTable"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
{i18n.translate('xpack.apm.transactionsTable.title', {
|
||||
defaultMessage: 'Transactions',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{!hideViewTransactionsLink && (
|
||||
{!hideTitle && (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<TransactionOverviewLink
|
||||
serviceName={serviceName}
|
||||
latencyAggregationType={latencyAggregationType}
|
||||
transactionType={transactionType}
|
||||
>
|
||||
{i18n.translate('xpack.apm.transactionsTable.linkText', {
|
||||
defaultMessage: 'View transactions',
|
||||
})}
|
||||
</TransactionOverviewLink>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
{i18n.translate('xpack.apm.transactionsTable.title', {
|
||||
defaultMessage: 'Transactions',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{!hideViewTransactionsLink && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<TransactionOverviewLink
|
||||
serviceName={serviceName}
|
||||
latencyAggregationType={latencyAggregationType}
|
||||
transactionType={transactionType}
|
||||
>
|
||||
{i18n.translate('xpack.apm.transactionsTable.linkText', {
|
||||
defaultMessage: 'View transactions',
|
||||
})}
|
||||
</TransactionOverviewLink>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
{showMaxTransactionGroupsExceededWarning && maxTransactionGroupsExceeded && (
|
||||
<EuiFlexItem>
|
||||
<EuiCallOut
|
||||
|
|
|
@ -88,6 +88,10 @@ const documentTypeConfigMap: Record<
|
|||
},
|
||||
}),
|
||||
},
|
||||
[ApmDocumentType.ErrorEvent]: {
|
||||
processorEvent: ProcessorEvent.error,
|
||||
rollupIntervals: [RollupInterval.None],
|
||||
},
|
||||
};
|
||||
|
||||
type DocumentTypeConfigOf<TApmDocumentType extends ApmDocumentType> =
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
/*
|
||||
* 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 {
|
||||
kqlQuery,
|
||||
rangeQuery,
|
||||
termQuery,
|
||||
} from '@kbn/observability-plugin/server';
|
||||
import { keyBy } from 'lodash';
|
||||
import { getBucketSize } from '../../../common/utils/get_bucket_size';
|
||||
import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms';
|
||||
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_DURATION,
|
||||
} from '../../../common/es_fields/apm';
|
||||
import { getLatencyValue } from '../../lib/helpers/latency_aggregation_type';
|
||||
import { LatencyAggregationType } from '../../../common/latency_aggregation_types';
|
||||
import { offsetPreviousPeriodCoordinates } from '../../../common/utils/offset_previous_period_coordinate';
|
||||
import { Coordinate } from '../../../typings/timeseries';
|
||||
import { ApmDocumentType } from '../../../common/document_type';
|
||||
import { RollupInterval } from '../../../common/rollup';
|
||||
|
||||
interface MobileDetailedStatistics {
|
||||
fieldName: string;
|
||||
latency: Coordinate[];
|
||||
throughput: Coordinate[];
|
||||
}
|
||||
|
||||
export interface MobileDetailedStatisticsResponse {
|
||||
currentPeriod: Record<string, MobileDetailedStatistics>;
|
||||
previousPeriod: Record<string, MobileDetailedStatistics>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
kuery: string;
|
||||
apmEventClient: APMEventClient;
|
||||
serviceName: string;
|
||||
environment: string;
|
||||
start: number;
|
||||
end: number;
|
||||
field: string;
|
||||
fieldValues: string[];
|
||||
offset?: string;
|
||||
}
|
||||
|
||||
async function getMobileDetailedStatisticsByField({
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
field,
|
||||
fieldValues,
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
offset,
|
||||
}: Props) {
|
||||
const { startWithOffset, endWithOffset } = getOffsetInMs({
|
||||
start,
|
||||
end,
|
||||
offset,
|
||||
});
|
||||
|
||||
const { intervalString } = getBucketSize({
|
||||
start: startWithOffset,
|
||||
end: endWithOffset,
|
||||
minBucketSize: 60,
|
||||
});
|
||||
|
||||
const response = await apmEventClient.search(
|
||||
`get_mobile_detailed_statistics_by_field`,
|
||||
{
|
||||
apm: {
|
||||
sources: [
|
||||
{
|
||||
documentType: ApmDocumentType.TransactionEvent,
|
||||
rollupInterval: RollupInterval.None,
|
||||
},
|
||||
],
|
||||
},
|
||||
body: {
|
||||
track_total_hits: false,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
...rangeQuery(startWithOffset, endWithOffset),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
detailed_statistics: {
|
||||
terms: {
|
||||
field,
|
||||
include: fieldValues,
|
||||
size: fieldValues.length,
|
||||
},
|
||||
aggs: {
|
||||
timeseries: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: intervalString,
|
||||
min_doc_count: 0,
|
||||
extended_bounds: {
|
||||
min: startWithOffset,
|
||||
max: endWithOffset,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
latency: {
|
||||
avg: {
|
||||
field: TRANSACTION_DURATION,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const buckets = response.aggregations?.detailed_statistics.buckets ?? [];
|
||||
|
||||
return buckets.map((bucket) => {
|
||||
const fieldName = bucket.key as string;
|
||||
const latency = bucket.timeseries.buckets.map((timeseriesBucket) => ({
|
||||
x: timeseriesBucket.key,
|
||||
y: getLatencyValue({
|
||||
latencyAggregationType: LatencyAggregationType.avg,
|
||||
aggregation: timeseriesBucket.latency,
|
||||
}),
|
||||
}));
|
||||
const throughput = bucket.timeseries.buckets.map((timeseriesBucket) => ({
|
||||
x: timeseriesBucket.key,
|
||||
y: timeseriesBucket.doc_count,
|
||||
}));
|
||||
|
||||
return {
|
||||
fieldName,
|
||||
latency,
|
||||
throughput,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function getMobileDetailedStatisticsByFieldPeriods({
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
field,
|
||||
fieldValues,
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
offset,
|
||||
}: Props): Promise<MobileDetailedStatisticsResponse> {
|
||||
const commonProps = {
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
field,
|
||||
fieldValues,
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
};
|
||||
|
||||
const currentPeriodPromise = getMobileDetailedStatisticsByField({
|
||||
...commonProps,
|
||||
});
|
||||
|
||||
const previousPeriodPromise = offset
|
||||
? getMobileDetailedStatisticsByField({
|
||||
...commonProps,
|
||||
offset,
|
||||
})
|
||||
: [];
|
||||
|
||||
const [currentPeriod, previousPeriod] = await Promise.all([
|
||||
currentPeriodPromise,
|
||||
previousPeriodPromise,
|
||||
]);
|
||||
|
||||
const firstCurrentPeriod = currentPeriod?.[0];
|
||||
return {
|
||||
currentPeriod: keyBy(currentPeriod, 'fieldName'),
|
||||
previousPeriod: keyBy(
|
||||
previousPeriod.map((data) => {
|
||||
return {
|
||||
...data,
|
||||
latency: offsetPreviousPeriodCoordinates({
|
||||
currentPeriodTimeseries: firstCurrentPeriod?.latency,
|
||||
previousPeriodTimeseries: data.latency,
|
||||
}),
|
||||
throughput: offsetPreviousPeriodCoordinates({
|
||||
currentPeriodTimeseries: firstCurrentPeriod?.throughput,
|
||||
previousPeriodTimeseries: data.throughput,
|
||||
}),
|
||||
};
|
||||
}),
|
||||
'fieldName'
|
||||
),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* 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 { merge } from 'lodash';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
SESSION_ID,
|
||||
TRANSACTION_DURATION,
|
||||
ERROR_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 { getLatencyValue } from '../../lib/helpers/latency_aggregation_type';
|
||||
import { LatencyAggregationType } from '../../../common/latency_aggregation_types';
|
||||
import { calculateThroughputWithRange } from '../../lib/helpers/calculate_throughput';
|
||||
import { ApmDocumentType } from '../../../common/document_type';
|
||||
import { RollupInterval } from '../../../common/rollup';
|
||||
|
||||
interface Props {
|
||||
kuery: string;
|
||||
apmEventClient: APMEventClient;
|
||||
serviceName: string;
|
||||
environment: string;
|
||||
start: number;
|
||||
end: number;
|
||||
field: string;
|
||||
}
|
||||
|
||||
export interface MobileMainStatisticsResponse {
|
||||
mainStatistics: Array<{
|
||||
name: string | number;
|
||||
latency: number | null;
|
||||
throughput: number;
|
||||
crashRate?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function getMobileMainStatisticsByField({
|
||||
kuery,
|
||||
apmEventClient,
|
||||
serviceName,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
field,
|
||||
}: Props) {
|
||||
async function getMobileTransactionEventStatistics() {
|
||||
const response = await apmEventClient.search(
|
||||
`get_mobile_main_statistics_by_field`,
|
||||
{
|
||||
apm: {
|
||||
sources: [
|
||||
{
|
||||
documentType: ApmDocumentType.TransactionEvent,
|
||||
rollupInterval: RollupInterval.None,
|
||||
},
|
||||
],
|
||||
},
|
||||
body: {
|
||||
track_total_hits: false,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
main_statistics: {
|
||||
terms: {
|
||||
field,
|
||||
size: 1000,
|
||||
},
|
||||
aggs: {
|
||||
latency: {
|
||||
avg: {
|
||||
field: TRANSACTION_DURATION,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
response.aggregations?.main_statistics.buckets.map((bucket) => {
|
||||
return {
|
||||
name: bucket.key,
|
||||
latency: getLatencyValue({
|
||||
latencyAggregationType: LatencyAggregationType.avg,
|
||||
aggregation: bucket.latency,
|
||||
}),
|
||||
throughput: calculateThroughputWithRange({
|
||||
start,
|
||||
end,
|
||||
value: bucket.doc_count,
|
||||
}),
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
async function getMobileErrorEventStatistics() {
|
||||
const response = await apmEventClient.search(
|
||||
`get_mobile_transaction_events_main_statistics_by_field`,
|
||||
{
|
||||
apm: {
|
||||
sources: [
|
||||
{
|
||||
documentType: ApmDocumentType.ErrorEvent,
|
||||
rollupInterval: RollupInterval.None,
|
||||
},
|
||||
],
|
||||
},
|
||||
body: {
|
||||
track_total_hits: false,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
main_statistics: {
|
||||
terms: {
|
||||
field,
|
||||
size: 1000,
|
||||
},
|
||||
aggs: {
|
||||
sessions: {
|
||||
cardinality: {
|
||||
field: SESSION_ID,
|
||||
},
|
||||
},
|
||||
crashes: {
|
||||
filter: {
|
||||
term: {
|
||||
[ERROR_TYPE]: 'crash',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
return (
|
||||
response.aggregations?.main_statistics.buckets.map((bucket) => {
|
||||
return {
|
||||
name: bucket.key,
|
||||
crashRate: bucket.crashes.doc_count / bucket.sessions.value ?? 0,
|
||||
};
|
||||
}) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
const [transactioEventStatistics, errorEventStatistics] = await Promise.all([
|
||||
getMobileTransactionEventStatistics(),
|
||||
getMobileErrorEventStatistics(),
|
||||
]);
|
||||
|
||||
const mainStatistics = merge(transactioEventStatistics, errorEventStatistics);
|
||||
|
||||
return { mainStatistics };
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { toNumberRt } from '@kbn/io-ts-utils';
|
||||
import { jsonRt, toNumberRt } from '@kbn/io-ts-utils';
|
||||
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
|
||||
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
|
||||
import { environmentRt, kueryRt, rangeRt } from '../default_api_types';
|
||||
|
@ -26,6 +26,14 @@ import {
|
|||
getMobileTermsByField,
|
||||
MobileTermsByFieldResponse,
|
||||
} from './get_mobile_terms_by_field';
|
||||
import {
|
||||
getMobileMainStatisticsByField,
|
||||
MobileMainStatisticsResponse,
|
||||
} from './get_mobile_main_statistics_by_field';
|
||||
import {
|
||||
getMobileDetailedStatisticsByFieldPeriods,
|
||||
MobileDetailedStatisticsResponse,
|
||||
} from './get_mobile_detailed_statistics_by_field';
|
||||
import {
|
||||
getMobileMostUsedCharts,
|
||||
MobileMostUsedChartResponse,
|
||||
|
@ -329,6 +337,84 @@ const mobileTermsByFieldRoute = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const mobileMainStatisticsByField = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/main_statistics',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: t.intersection([
|
||||
kueryRt,
|
||||
rangeRt,
|
||||
environmentRt,
|
||||
t.type({
|
||||
field: t.string,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
options: {
|
||||
tags: ['access:apm'],
|
||||
},
|
||||
handler: async (resources): Promise<MobileMainStatisticsResponse> => {
|
||||
const apmEventClient = await getApmEventClient(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const { kuery, environment, start, end, field } = params.query;
|
||||
|
||||
return await getMobileMainStatisticsByField({
|
||||
kuery,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
serviceName,
|
||||
apmEventClient,
|
||||
field,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const mobileDetailedStatisticsByField = createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /internal/apm/mobile-services/{serviceName}/detailed_statistics',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: t.intersection([
|
||||
kueryRt,
|
||||
rangeRt,
|
||||
offsetRt,
|
||||
environmentRt,
|
||||
t.type({
|
||||
field: t.string,
|
||||
fieldValues: jsonRt.pipe(t.array(t.string)),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
options: {
|
||||
tags: ['access:apm'],
|
||||
},
|
||||
handler: async (resources): Promise<MobileDetailedStatisticsResponse> => {
|
||||
const apmEventClient = await getApmEventClient(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const { kuery, environment, start, end, field, offset, fieldValues } =
|
||||
params.query;
|
||||
|
||||
return await getMobileDetailedStatisticsByFieldPeriods({
|
||||
kuery,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
serviceName,
|
||||
apmEventClient,
|
||||
field,
|
||||
fieldValues,
|
||||
offset,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const mobileRouteRepository = {
|
||||
...mobileFiltersRoute,
|
||||
...mobileChartsRoute,
|
||||
|
@ -337,4 +423,6 @@ export const mobileRouteRepository = {
|
|||
...mobileStatsRoute,
|
||||
...mobileLocationStatsRoute,
|
||||
...mobileTermsByFieldRoute,
|
||||
...mobileMainStatisticsByField,
|
||||
...mobileDetailedStatisticsByField,
|
||||
};
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
|
||||
export const SERVICE_VERSIONS = ['2.3', '1.2', '1.1'];
|
||||
|
||||
export async function generateMobileData({
|
||||
start,
|
||||
end,
|
||||
|
@ -22,7 +24,7 @@ export async function generateMobileData({
|
|||
environment: 'production',
|
||||
agentName: 'android/java',
|
||||
})
|
||||
.mobileDevice({ serviceVersion: '2.3' })
|
||||
.mobileDevice({ serviceVersion: SERVICE_VERSIONS[0] })
|
||||
.deviceInfo({
|
||||
manufacturer: 'Samsung',
|
||||
modelIdentifier: 'SM-G973F',
|
||||
|
@ -52,7 +54,7 @@ export async function generateMobileData({
|
|||
environment: 'production',
|
||||
agentName: 'android/java',
|
||||
})
|
||||
.mobileDevice({ serviceVersion: '1.2' })
|
||||
.mobileDevice({ serviceVersion: SERVICE_VERSIONS[1] })
|
||||
.deviceInfo({
|
||||
manufacturer: 'Samsung',
|
||||
modelIdentifier: 'SM-G930F',
|
||||
|
@ -89,7 +91,7 @@ export async function generateMobileData({
|
|||
environment: 'production',
|
||||
agentName: 'android/java',
|
||||
})
|
||||
.mobileDevice({ serviceVersion: '1.1' })
|
||||
.mobileDevice({ serviceVersion: SERVICE_VERSIONS[2] })
|
||||
.deviceInfo({
|
||||
manufacturer: 'Huawei',
|
||||
modelIdentifier: 'HUAWEI P2-0000',
|
||||
|
@ -222,6 +224,7 @@ export async function generateMobileData({
|
|||
return [
|
||||
galaxy10
|
||||
.transaction('Start View - View Appearing', 'Android Activity')
|
||||
.errors(galaxy10.crash({ message: 'error' }).timestamp(timestamp))
|
||||
.timestamp(timestamp)
|
||||
.duration(500)
|
||||
.success()
|
||||
|
@ -265,6 +268,7 @@ export async function generateMobileData({
|
|||
),
|
||||
huaweiP2
|
||||
.transaction('Start View - View Appearing', 'huaweiP2 Activity')
|
||||
.errors(huaweiP2.crash({ message: 'error' }).timestamp(timestamp))
|
||||
.timestamp(timestamp)
|
||||
.duration(20)
|
||||
.success()
|
||||
|
@ -292,6 +296,7 @@ export async function generateMobileData({
|
|||
),
|
||||
galaxy7
|
||||
.transaction('Start View - View Appearing', 'Android Activity')
|
||||
.errors(galaxy7.crash({ message: 'error' }).timestamp(timestamp))
|
||||
.timestamp(timestamp)
|
||||
.duration(20)
|
||||
.success()
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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 { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
|
||||
import { isEmpty } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { generateMobileData, SERVICE_VERSIONS } from './generate_mobile_data';
|
||||
|
||||
type MobileDetailedStatisticsResponse =
|
||||
APIReturnType<'GET /internal/apm/mobile-services/{serviceName}/detailed_statistics'>;
|
||||
|
||||
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 getMobileDetailedStatisticsByField({
|
||||
environment = ENVIRONMENT_ALL.value,
|
||||
kuery = '',
|
||||
serviceName,
|
||||
field,
|
||||
offset,
|
||||
}: {
|
||||
environment?: string;
|
||||
kuery?: string;
|
||||
serviceName: string;
|
||||
field: string;
|
||||
offset?: string;
|
||||
}) {
|
||||
return await apmApiClient
|
||||
.readUser({
|
||||
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/detailed_statistics',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
environment,
|
||||
start: moment(end).subtract(7, 'minutes').toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
offset,
|
||||
kuery,
|
||||
field,
|
||||
fieldValues: JSON.stringify(SERVICE_VERSIONS),
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ body }) => body);
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Mobile detailed statistics when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
describe('when no data', () => {
|
||||
it('handles empty state', async () => {
|
||||
const response = await getMobileDetailedStatisticsByField({
|
||||
serviceName: 'foo',
|
||||
field: 'service.version',
|
||||
});
|
||||
expect(response).to.be.eql({ currentPeriod: {}, previousPeriod: {} });
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
registry.when(
|
||||
'Mobile detailed statistics when data is loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
before(async () => {
|
||||
await generateMobileData({
|
||||
synthtraceEsClient,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
describe('when comparison is disable', () => {
|
||||
it('returns current period data only', async () => {
|
||||
const response = await getMobileDetailedStatisticsByField({
|
||||
serviceName: 'synth-android',
|
||||
environment: 'production',
|
||||
field: 'service.version',
|
||||
});
|
||||
expect(isEmpty(response.currentPeriod)).to.be.equal(false);
|
||||
expect(isEmpty(response.previousPeriod)).to.be.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when comparison is enable', () => {
|
||||
let mobiledetailedStatisticResponse: MobileDetailedStatisticsResponse;
|
||||
|
||||
before(async () => {
|
||||
mobiledetailedStatisticResponse = await getMobileDetailedStatisticsByField({
|
||||
serviceName: 'synth-android',
|
||||
environment: 'production',
|
||||
field: 'service.version',
|
||||
offset: '8m',
|
||||
});
|
||||
});
|
||||
it('returns some data for both periods', async () => {
|
||||
expect(isEmpty(mobiledetailedStatisticResponse.currentPeriod)).to.be.equal(false);
|
||||
expect(isEmpty(mobiledetailedStatisticResponse.previousPeriod)).to.be.equal(false);
|
||||
});
|
||||
|
||||
it('returns same number of buckets for both periods', () => {
|
||||
const currentPeriod = mobiledetailedStatisticResponse.currentPeriod[SERVICE_VERSIONS[0]];
|
||||
const previousPeriod =
|
||||
mobiledetailedStatisticResponse.previousPeriod[SERVICE_VERSIONS[0]];
|
||||
|
||||
[
|
||||
[currentPeriod.latency, previousPeriod.latency],
|
||||
[currentPeriod.throughput, previousPeriod.throughput],
|
||||
].forEach(([currentTimeseries, previousTimeseries]) => {
|
||||
expect(currentTimeseries.length).to.equal(previousTimeseries.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* 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 { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { generateMobileData } from './generate_mobile_data';
|
||||
|
||||
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 getMobileMainStatisticsByField({
|
||||
environment = ENVIRONMENT_ALL.value,
|
||||
kuery = '',
|
||||
serviceName,
|
||||
field,
|
||||
}: {
|
||||
environment?: string;
|
||||
kuery?: string;
|
||||
serviceName: string;
|
||||
field: string;
|
||||
}) {
|
||||
return await apmApiClient
|
||||
.readUser({
|
||||
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/main_statistics',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
environment,
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
kuery,
|
||||
field,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ body }) => body);
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Mobile main statistics when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
describe('when no data', () => {
|
||||
it('handles empty state', async () => {
|
||||
const response = await getMobileMainStatisticsByField({
|
||||
serviceName: 'foo',
|
||||
field: 'service.version',
|
||||
});
|
||||
expect(response.mainStatistics.length).to.be(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
registry.when('Mobile main statistics', { config: 'basic', archives: [] }, () => {
|
||||
before(async () => {
|
||||
await generateMobileData({
|
||||
synthtraceEsClient,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
describe('when data is loaded', () => {
|
||||
it('returns the correct data for App version', async () => {
|
||||
const response = await getMobileMainStatisticsByField({
|
||||
serviceName: 'synth-android',
|
||||
environment: 'production',
|
||||
field: 'service.version',
|
||||
});
|
||||
const fieldValues = response.mainStatistics.map((item) => item.name);
|
||||
|
||||
expect(fieldValues).to.be.eql(['1.1', '1.2', '2.3']);
|
||||
|
||||
const latencyValues = response.mainStatistics.map((item) => item.latency);
|
||||
|
||||
expect(latencyValues).to.be.eql([172000, 20000, 20000]);
|
||||
|
||||
const throughputValues = response.mainStatistics.map((item) => item.throughput);
|
||||
expect(throughputValues).to.be.eql([
|
||||
1.0000011111123457, 0.20000022222246913, 0.20000022222246913,
|
||||
]);
|
||||
});
|
||||
it('returns the correct data for Os version', async () => {
|
||||
const response = await getMobileMainStatisticsByField({
|
||||
serviceName: 'synth-android',
|
||||
environment: 'production',
|
||||
field: 'host.os.version',
|
||||
});
|
||||
|
||||
const fieldValues = response.mainStatistics.map((item) => item.name);
|
||||
|
||||
expect(fieldValues).to.be.eql(['10']);
|
||||
|
||||
const latencyValues = response.mainStatistics.map((item) => item.latency);
|
||||
|
||||
expect(latencyValues).to.be.eql([128571.42857142857]);
|
||||
|
||||
const throughputValues = response.mainStatistics.map((item) => item.throughput);
|
||||
expect(throughputValues).to.be.eql([1.4000015555572838]);
|
||||
});
|
||||
it('returns the correct data for Devices', async () => {
|
||||
const response = await getMobileMainStatisticsByField({
|
||||
serviceName: 'synth-android',
|
||||
environment: 'production',
|
||||
field: 'device.model.identifier',
|
||||
});
|
||||
const fieldValues = response.mainStatistics.map((item) => item.name);
|
||||
|
||||
expect(fieldValues).to.be.eql([
|
||||
'HUAWEI P2-0000',
|
||||
'SM-G930F',
|
||||
'SM-G973F',
|
||||
'Pixel 7 Pro',
|
||||
'Pixel 8',
|
||||
'SM-G930F',
|
||||
]);
|
||||
|
||||
const latencyValues = response.mainStatistics.map((item) => item.latency);
|
||||
|
||||
expect(latencyValues).to.be.eql([400000, 20000, 20000, 20000, 20000, 20000]);
|
||||
|
||||
const throughputValues = response.mainStatistics.map((item) => item.throughput);
|
||||
expect(throughputValues).to.be.eql([
|
||||
0.40000044444493826, 0.20000022222246913, 0.20000022222246913, 0.20000022222246913,
|
||||
0.20000022222246913, 0.20000022222246913,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue