mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Average mobile app launch (#170773)
## Summary
Enabled the "Average app load time" panel in the mobile dashboard.

### 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 <kate@kpatticha.com>
This commit is contained in:
parent
e252b8548c
commit
5c44377de1
7 changed files with 200 additions and 15 deletions
|
@ -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,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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<number> };
|
||||
previousPeriod: { timeseries: Coordinate[]; value: Maybe<number> };
|
||||
}
|
||||
|
||||
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<AvgLaunchTimeTimeseries> {
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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<number> };
|
||||
requests: { timeseries: Timeseries[]; value: Maybe<number> };
|
||||
crashRate: { timeseries: Timeseries[]; value: Maybe<number> };
|
||||
launchTimes: { timeseries: Timeseries[]; value: Maybe<number> };
|
||||
}
|
||||
|
||||
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([
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue