From 5c44377de17f8a2da8790c5308822e1fc4d8aa95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar?= <56847527+LikeTheSalad@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:16:09 +0100 Subject: [PATCH] Average mobile app launch (#170773) ## Summary Enabled the "Average app load time" panel in the mobile dashboard. ![Screenshot 2023-11-08 at 18 35 12](https://github.com/elastic/kibana/assets/56847527/4ae1b05b-d71b-47e2-9f11-a884251de975) ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Katerina --- .../mobile/service_overview/stats/stats.tsx | 23 +-- .../mobile/get_mobile_average_launch_time.ts | 161 ++++++++++++++++++ .../server/routes/mobile/get_mobile_stats.ts | 11 +- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../tests/mobile/mobile_stats.spec.ts | 17 +- 7 files changed, 200 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/apm/server/routes/mobile/get_mobile_average_launch_time.ts diff --git a/x-pack/plugins/apm/public/components/app/mobile/service_overview/stats/stats.tsx b/x-pack/plugins/apm/public/components/app/mobile/service_overview/stats/stats.tsx index a0caf4b3002ac8..85e29bbe53e399 100644 --- a/x-pack/plugins/apm/public/components/app/mobile/service_overview/stats/stats.tsx +++ b/x-pack/plugins/apm/public/components/app/mobile/service_overview/stats/stats.tsx @@ -7,9 +7,9 @@ import { MetricDatum, MetricTrendShape } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { - EuiIcon, EuiFlexGroup, EuiFlexItem, + EuiIcon, EuiLoadingSpinner, } from '@elastic/eui'; import React, { useCallback } from 'react'; @@ -17,9 +17,9 @@ import { useTheme } from '@kbn/observability-shared-plugin/public'; import { NOT_AVAILABLE_LABEL } from '../../../../../../common/i18n'; import { useAnyOfApmParams } from '../../../../../hooks/use_apm_params'; import { - useFetcher, FETCH_STATUS, isPending, + useFetcher, } from '../../../../../hooks/use_fetcher'; import { MetricItem } from './metric_item'; import { usePreviousPeriodLabel } from '../../../../../hooks/use_previous_period_text'; @@ -120,17 +120,20 @@ export function MobileStats({ trendShape: MetricTrendShape.Area, }, { - color: euiTheme.eui.euiColorDisabled, + color: euiTheme.eui.euiColorLightestShade, title: i18n.translate('xpack.apm.mobile.metrics.load.time', { - defaultMessage: 'Slowest App load time', - }), - subtitle: i18n.translate('xpack.apm.mobile.coming.soon', { - defaultMessage: 'Coming Soon', + defaultMessage: 'Average app load time', }), icon: getIcon('visGauge'), - value: 'N/A', - valueFormatter: (value: number) => valueFormatter(value, 's'), - trend: [], + value: data?.currentPeriod?.launchTimes?.value ?? NaN, + valueFormatter: (value: number) => + Number.isNaN(value) + ? NOT_AVAILABLE_LABEL + : valueFormatter(Number(value.toFixed(1)), 'ms'), + trend: data?.currentPeriod?.launchTimes?.timeseries, + extra: getComparisonValueFormatter( + data?.previousPeriod.launchTimes?.value?.toFixed(1) + ), trendShape: MetricTrendShape.Area, }, { diff --git a/x-pack/plugins/apm/server/routes/mobile/get_mobile_average_launch_time.ts b/x-pack/plugins/apm/server/routes/mobile/get_mobile_average_launch_time.ts new file mode 100644 index 00000000000000..e711d5c72fb3bd --- /dev/null +++ b/x-pack/plugins/apm/server/routes/mobile/get_mobile_average_launch_time.ts @@ -0,0 +1,161 @@ +/* + * 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 { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { + kqlQuery, + rangeQuery, + termQuery, +} from '@kbn/observability-plugin/server'; +import { offsetPreviousPeriodCoordinates } from '../../../common/utils/offset_previous_period_coordinate'; +import { APP_LAUNCH_TIME, SERVICE_NAME } from '../../../common/es_fields/apm'; +import { environmentQuery } from '../../../common/utils/environment_query'; +import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms'; +import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; +import { getBucketSize } from '../../../common/utils/get_bucket_size'; +import { Coordinate } from '../../../typings/timeseries'; +import { Maybe } from '../../../typings/common'; + +export interface AvgLaunchTimeTimeseries { + currentPeriod: { timeseries: Coordinate[]; value: Maybe }; + previousPeriod: { timeseries: Coordinate[]; value: Maybe }; +} + +interface Props { + apmEventClient: APMEventClient; + serviceName: string; + transactionName?: string; + environment: string; + start: number; + end: number; + kuery: string; + offset?: string; +} + +async function getAvgLaunchTimeTimeseries({ + apmEventClient, + serviceName, + transactionName, + environment, + start, + end, + kuery, + offset, +}: Props) { + const { startWithOffset, endWithOffset } = getOffsetInMs({ + start, + end, + offset, + }); + + const { intervalString } = getBucketSize({ + start: startWithOffset, + end: endWithOffset, + minBucketSize: 60, + }); + + const aggs = { + launchTimeAvg: { + avg: { field: APP_LAUNCH_TIME }, + }, + }; + + const response = await apmEventClient.search('get_mobile_launch_time', { + apm: { + events: [ProcessorEvent.metric], + }, + body: { + track_total_hits: false, + size: 0, + query: { + bool: { + filter: [ + ...termQuery(SERVICE_NAME, serviceName), + ...rangeQuery(startWithOffset, endWithOffset), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ], + }, + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { min: startWithOffset, max: endWithOffset }, + }, + aggs, + }, + ...aggs, + }, + }, + }); + + const timeseries = + response?.aggregations?.timeseries.buckets.map((bucket) => { + return { + x: bucket.key, + y: bucket.launchTimeAvg.value, + }; + }) ?? []; + + return { + timeseries, + value: response.aggregations?.launchTimeAvg?.value, + }; +} + +export async function getMobileAvgLaunchTime({ + kuery, + apmEventClient, + serviceName, + transactionName, + environment, + start, + end, + offset, +}: Props): Promise { + const options = { + serviceName, + transactionName, + apmEventClient, + kuery, + environment, + }; + + const currentPeriodPromise = getAvgLaunchTimeTimeseries({ + ...options, + start, + end, + }); + + const previousPeriodPromise = offset + ? getAvgLaunchTimeTimeseries({ + ...options, + start, + end, + offset, + }) + : { timeseries: [], value: null }; + + const [currentPeriod, previousPeriod] = await Promise.all([ + currentPeriodPromise, + previousPeriodPromise, + ]); + + return { + currentPeriod, + previousPeriod: { + timeseries: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: currentPeriod.timeseries, + previousPeriodTimeseries: previousPeriod.timeseries, + }), + value: previousPeriod?.value, + }, + }; +} diff --git a/x-pack/plugins/apm/server/routes/mobile/get_mobile_stats.ts b/x-pack/plugins/apm/server/routes/mobile/get_mobile_stats.ts index 071487298ab7a2..116117426405c2 100644 --- a/x-pack/plugins/apm/server/routes/mobile/get_mobile_stats.ts +++ b/x-pack/plugins/apm/server/routes/mobile/get_mobile_stats.ts @@ -10,16 +10,19 @@ import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms'; import { getMobileSessions } from './get_mobile_sessions'; import { getMobileHttpRequests } from './get_mobile_http_requests'; import { getMobileCrashRate } from './get_mobile_crash_rate'; +import { getMobileAvgLaunchTime } from './get_mobile_average_launch_time'; import { Maybe } from '../../../typings/common'; export interface Timeseries { x: number; y: number; } + interface MobileStats { sessions: { timeseries: Timeseries[]; value: Maybe }; requests: { timeseries: Timeseries[]; value: Maybe }; crashRate: { timeseries: Timeseries[]; value: Maybe }; + launchTimes: { timeseries: Timeseries[]; value: Maybe }; } export interface MobilePeriodStats { @@ -62,10 +65,11 @@ async function getMobileStats({ offset, }; - const [sessions, httpRequests, crashes] = await Promise.all([ + const [sessions, httpRequests, crashes, launchTimeAvg] = await Promise.all([ getMobileSessions({ ...commonProps }), getMobileHttpRequests({ ...commonProps }), getMobileCrashRate({ ...commonProps }), + getMobileAvgLaunchTime({ ...commonProps }), ]); return { @@ -89,6 +93,10 @@ async function getMobileStats({ }; }) as Timeseries[], }, + launchTimes: { + value: launchTimeAvg.currentPeriod.value, + timeseries: launchTimeAvg.currentPeriod.timeseries as Timeseries[], + }, }; } @@ -123,6 +131,7 @@ export async function getMobileStatsPeriods({ sessions: { timeseries: [], value: null }, requests: { timeseries: [], value: null }, crashRate: { timeseries: [], value: null }, + launchTimes: { timeseries: [], value: null }, }; const [currentPeriod, previousPeriod] = await Promise.all([ diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 9024c08ddcc3df..d00b542c581313 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -9023,7 +9023,6 @@ "xpack.apm.mobile.charts.nct": "Type de connexion réseau", "xpack.apm.mobile.charts.noResultsFound": "Résultat introuvable", "xpack.apm.mobile.charts.osVersion": "Version du système d'exploitation", - "xpack.apm.mobile.coming.soon": "Bientôt disponible", "xpack.apm.mobile.filters.appVersion": "Version de l'application", "xpack.apm.mobile.filters.device": "Appareil", "xpack.apm.mobile.filters.nct": "NCT", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6f0fada5b569d1..1ce0c2df38db57 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9038,7 +9038,6 @@ "xpack.apm.mobile.charts.nct": "ネットワーク接続タイプ", "xpack.apm.mobile.charts.noResultsFound": "結果が見つかりませんでした", "xpack.apm.mobile.charts.osVersion": "OSバージョン", - "xpack.apm.mobile.coming.soon": "まもなくリリース", "xpack.apm.mobile.filters.appVersion": "アプリバージョン", "xpack.apm.mobile.filters.device": "デバイス", "xpack.apm.mobile.filters.nct": "NCT", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 307ba7316f7fcd..2fd8d38a579a57 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9037,7 +9037,6 @@ "xpack.apm.mobile.charts.nct": "网络连接类型", "xpack.apm.mobile.charts.noResultsFound": "找不到结果", "xpack.apm.mobile.charts.osVersion": "操作系统版本", - "xpack.apm.mobile.coming.soon": "即将推出", "xpack.apm.mobile.filters.appVersion": "应用版本", "xpack.apm.mobile.filters.device": "设备", "xpack.apm.mobile.filters.nct": "NCT", diff --git a/x-pack/test/apm_api_integration/tests/mobile/mobile_stats.spec.ts b/x-pack/test/apm_api_integration/tests/mobile/mobile_stats.spec.ts index edc852d97ad2a0..f7bb8f328d220b 100644 --- a/x-pack/test/apm_api_integration/tests/mobile/mobile_stats.spec.ts +++ b/x-pack/test/apm_api_integration/tests/mobile/mobile_stats.spec.ts @@ -10,7 +10,7 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; 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 { sumBy, meanBy } from 'lodash'; +import { meanBy, sumBy } from 'lodash'; import { FtrProviderContext } from '../../common/ftr_provider_context'; type MobileStats = APIReturnType<'GET /internal/apm/mobile-services/{serviceName}/stats'>; @@ -101,6 +101,7 @@ async function generateData({ galaxy10.startNewSession(); huaweiP2.startNewSession(); return [ + galaxy10.appMetrics({ 'application.launch.time': 100 }).timestamp(timestamp), galaxy10 .transaction('Start View - View Appearing', 'Android Activity') .errors(galaxy10.crash({ message: 'error C' }).timestamp(timestamp)) @@ -224,6 +225,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(value).to.be(timeseriesMean); }); + it('returns same launch times', () => { + const { value, timeseries } = response.currentPeriod.launchTimes; + const timeseriesMean = meanBy( + timeseries.filter((bucket) => bucket.y !== null), + 'y' + ); + expect(value).to.be(timeseriesMean); + }); }); describe('when filters are applied', () => { @@ -237,6 +246,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.currentPeriod.sessions.value).to.eql(0); expect(response.currentPeriod.requests.value).to.eql(0); expect(response.currentPeriod.crashRate.value).to.eql(0); + expect(response.currentPeriod.launchTimes.value).to.eql(null); expect(response.currentPeriod.sessions.timeseries.every((item) => item.y === 0)).to.eql( true @@ -247,6 +257,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.currentPeriod.crashRate.timeseries.every((item) => item.y === 0)).to.eql( true ); + expect( + response.currentPeriod.launchTimes.timeseries.every((item) => item.y === null) + ).to.eql(true); }); it('returns the correct values when single filter is applied', async () => { @@ -259,6 +272,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.currentPeriod.sessions.value).to.eql(3); expect(response.currentPeriod.requests.value).to.eql(0); expect(response.currentPeriod.crashRate.value).to.eql(3); + expect(response.currentPeriod.launchTimes.value).to.eql(null); }); it('returns the correct values when multiple filters are applied', async () => { @@ -269,6 +283,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.currentPeriod.sessions.value).to.eql(3); expect(response.currentPeriod.requests.value).to.eql(3); expect(response.currentPeriod.crashRate.value).to.eql(1); + expect(response.currentPeriod.launchTimes.value).to.eql(100); }); }); });