Skip to content

Commit

Permalink
Average mobile app launch (elastic#170773)
Browse files Browse the repository at this point in the history
## 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 <kate@kpatticha.com>
  • Loading branch information
3 people authored Nov 9, 2023
1 parent e252b85 commit 5c44377
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@
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';
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';
Expand Down Expand Up @@ -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,
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
},
};
}
11 changes: 10 additions & 1 deletion x-pack/plugins/apm/server/routes/mobile/get_mobile_stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -89,6 +93,10 @@ async function getMobileStats({
};
}) as Timeseries[],
},
launchTimes: {
value: launchTimeAvg.currentPeriod.value,
timeseries: launchTimeAvg.currentPeriod.timeseries as Timeseries[],
},
};
}

Expand Down Expand Up @@ -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([
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'>;
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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);
});
});
});
Expand Down

0 comments on commit 5c44377

Please sign in to comment.