mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[APM] Introduce mobile top metrics (#148330)
## Summary closes: #146854 https://user-images.githubusercontent.com/3369346/211276601-92fb9d6e-60fd-4ca3-be37-66daac08d7ac.mov   References for mobile fields (WIP) - events69a52f23d5/specs/agents/mobile/events.md (crashes)
- metrics69a52f23d5/specs/agents/mobile/metrics.md
### Notes Comparison is not included in this PR. Thus the color remains is the default one. Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
b05880e6ac
commit
61b8ef4363
11 changed files with 719 additions and 1 deletions
|
@ -368,7 +368,7 @@ const scenario: Scenario<ApmFields> = async ({ scenarioOpts, logger }) => {
|
|||
httpUrl: 'https://backend:1234/api/start',
|
||||
})
|
||||
.duration(800)
|
||||
.success()
|
||||
.failure()
|
||||
.timestamp(timestamp + 400)
|
||||
),
|
||||
device
|
||||
|
|
|
@ -11,6 +11,8 @@ exports[`Error AGENT_NAME 1`] = `"java"`;
|
|||
|
||||
exports[`Error AGENT_VERSION 1`] = `"agent version"`;
|
||||
|
||||
exports[`Error APP_LAUNCH_TIME 1`] = `undefined`;
|
||||
|
||||
exports[`Error CHILD_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Error CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
|
||||
|
@ -78,6 +80,8 @@ exports[`Error ERROR_LOG_MESSAGE 1`] = `undefined`;
|
|||
|
||||
exports[`Error ERROR_PAGE_URL 1`] = `undefined`;
|
||||
|
||||
exports[`Error EVENT_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error EVENT_OUTCOME 1`] = `undefined`;
|
||||
|
||||
exports[`Error FAAS_BILLED_DURATION 1`] = `undefined`;
|
||||
|
@ -210,6 +214,8 @@ exports[`Error SERVICE_RUNTIME_VERSION 1`] = `undefined`;
|
|||
|
||||
exports[`Error SERVICE_VERSION 1`] = `undefined`;
|
||||
|
||||
exports[`Error SESSION_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Error SPAN_ACTION 1`] = `undefined`;
|
||||
|
||||
exports[`Error SPAN_COMPOSITE_COMPRESSION_STRATEGY 1`] = `undefined`;
|
||||
|
@ -293,6 +299,8 @@ exports[`Span AGENT_NAME 1`] = `"java"`;
|
|||
|
||||
exports[`Span AGENT_VERSION 1`] = `"agent version"`;
|
||||
|
||||
exports[`Span APP_LAUNCH_TIME 1`] = `undefined`;
|
||||
|
||||
exports[`Span CHILD_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Span CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
|
||||
|
@ -351,6 +359,8 @@ exports[`Span ERROR_LOG_MESSAGE 1`] = `undefined`;
|
|||
|
||||
exports[`Span ERROR_PAGE_URL 1`] = `undefined`;
|
||||
|
||||
exports[`Span EVENT_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Span EVENT_OUTCOME 1`] = `"unknown"`;
|
||||
|
||||
exports[`Span FAAS_BILLED_DURATION 1`] = `undefined`;
|
||||
|
@ -475,6 +485,8 @@ exports[`Span SERVICE_RUNTIME_VERSION 1`] = `undefined`;
|
|||
|
||||
exports[`Span SERVICE_VERSION 1`] = `undefined`;
|
||||
|
||||
exports[`Span SESSION_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Span SPAN_ACTION 1`] = `"my action"`;
|
||||
|
||||
exports[`Span SPAN_COMPOSITE_COMPRESSION_STRATEGY 1`] = `undefined`;
|
||||
|
@ -558,6 +570,8 @@ exports[`Transaction AGENT_NAME 1`] = `"java"`;
|
|||
|
||||
exports[`Transaction AGENT_VERSION 1`] = `"agent version"`;
|
||||
|
||||
exports[`Transaction APP_LAUNCH_TIME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction CHILD_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction CLIENT_GEO_COUNTRY_ISO_CODE 1`] = `undefined`;
|
||||
|
@ -620,6 +634,8 @@ exports[`Transaction ERROR_LOG_MESSAGE 1`] = `undefined`;
|
|||
|
||||
exports[`Transaction ERROR_PAGE_URL 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction EVENT_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction EVENT_OUTCOME 1`] = `"unknown"`;
|
||||
|
||||
exports[`Transaction FAAS_BILLED_DURATION 1`] = `undefined`;
|
||||
|
@ -758,6 +774,8 @@ exports[`Transaction SERVICE_RUNTIME_VERSION 1`] = `undefined`;
|
|||
|
||||
exports[`Transaction SERVICE_VERSION 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction SESSION_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction SPAN_ACTION 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction SPAN_COMPOSITE_COMPRESSION_STRATEGY 1`] = `undefined`;
|
||||
|
|
|
@ -159,5 +159,8 @@ export const INDEX = '_index';
|
|||
// Mobile
|
||||
export const NETWORK_CONNECTION_TYPE = 'network.connection.type';
|
||||
export const DEVICE_MODEL_NAME = 'device.model.name';
|
||||
export const SESSION_ID = 'session.id';
|
||||
export const APP_LAUNCH_TIME = 'application.launch.time';
|
||||
export const EVENT_NAME = 'event.name';
|
||||
|
||||
export const CHILD_ID = 'child.id';
|
||||
|
|
|
@ -42,6 +42,7 @@ import { AggregatedTransactionsBadge } from '../../../shared/aggregated_transact
|
|||
import { LatencyChart } from '../../../shared/charts/latency_chart';
|
||||
import { useFiltersForEmbeddableCharts } from '../../../../hooks/use_filters_for_embeddable_charts';
|
||||
import { getKueryWithMobileFilters } from '../../../../../common/utils/get_kuery_with_mobile_filters';
|
||||
import { MobileStats } from './stats';
|
||||
/**
|
||||
* The height a chart should be if it's next to a table with 5 rows and a title.
|
||||
* Add the height of the pagination row.
|
||||
|
@ -146,6 +147,13 @@ export function MobileServiceOverview() {
|
|||
<AggregatedTransactionsBadge />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<MobileStats
|
||||
start={start}
|
||||
end={end}
|
||||
kuery={kueryWithMobileFilters}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={5}>
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 { MetricDatum, MetricTrendShape } from '@elastic/charts';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useTheme } from '@kbn/observability-plugin/public';
|
||||
import { useAnyOfApmParams } from '../../../../../hooks/use_apm_params';
|
||||
import { useFetcher, FETCH_STATUS } from '../../../../../hooks/use_fetcher';
|
||||
import { MetricItem } from './metric_item';
|
||||
|
||||
const valueFormatter = (value: number, suffix = '') => {
|
||||
return `${value} ${suffix}`;
|
||||
};
|
||||
|
||||
const getIcon =
|
||||
(type: string) =>
|
||||
({
|
||||
width = 20,
|
||||
height = 20,
|
||||
color,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
color: string;
|
||||
}) =>
|
||||
<EuiIcon type={type} width={width} height={height} fill={color} />;
|
||||
|
||||
export function MobileStats({
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
}: {
|
||||
start: string;
|
||||
end: string;
|
||||
kuery: string;
|
||||
}) {
|
||||
const euiTheme = useTheme();
|
||||
|
||||
const {
|
||||
path: { serviceName },
|
||||
query: { environment, transactionType },
|
||||
} = useAnyOfApmParams('/mobile-services/{serviceName}/overview');
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/mobile-services/{serviceName}/stats',
|
||||
{
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
transactionType,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
[start, end, environment, kuery, serviceName, transactionType]
|
||||
);
|
||||
|
||||
const metrics: MetricDatum[] = [
|
||||
{
|
||||
color: euiTheme.eui.euiColorLightestShade,
|
||||
title: i18n.translate('xpack.apm.mobile.metrics.crash.rate', {
|
||||
defaultMessage: 'Crash Rate',
|
||||
}),
|
||||
icon: getIcon('bug'),
|
||||
value: data?.crashCount?.value ?? NaN,
|
||||
valueFormatter: (value: number) => valueFormatter(value, 'cpm'),
|
||||
trend: data?.maxLoadTime?.timeseries,
|
||||
trendShape: MetricTrendShape.Area,
|
||||
},
|
||||
{
|
||||
color: euiTheme.eui.euiColorLightestShade,
|
||||
title: i18n.translate('xpack.apm.mobile.metrics.load.time', {
|
||||
defaultMessage: 'Slowest App load time',
|
||||
}),
|
||||
icon: getIcon('visGauge'),
|
||||
value: data?.maxLoadTime?.value ?? NaN,
|
||||
valueFormatter: (value: number) => valueFormatter(value, 's'),
|
||||
trend: data?.maxLoadTime.timeseries,
|
||||
trendShape: MetricTrendShape.Area,
|
||||
},
|
||||
{
|
||||
color: euiTheme.eui.euiColorLightestShade,
|
||||
title: i18n.translate('xpack.apm.mobile.metrics.sessions', {
|
||||
defaultMessage: 'Sessions',
|
||||
}),
|
||||
icon: getIcon('timeslider'),
|
||||
value: data?.sessions?.value ?? NaN,
|
||||
valueFormatter: (value: number) => valueFormatter(value),
|
||||
trend: data?.sessions.timeseries,
|
||||
trendShape: MetricTrendShape.Area,
|
||||
},
|
||||
{
|
||||
color: euiTheme.eui.euiColorLightestShade,
|
||||
title: i18n.translate('xpack.apm.mobile.metrics.http.requests', {
|
||||
defaultMessage: 'HTTP requests',
|
||||
}),
|
||||
icon: getIcon('kubernetesPod'),
|
||||
value: data?.requests?.value ?? NaN,
|
||||
valueFormatter: (value: number) => valueFormatter(value),
|
||||
trend: data?.requests.timeseries,
|
||||
trendShape: MetricTrendShape.Area,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
{metrics.map((metric, key) => (
|
||||
<EuiFlexItem>
|
||||
<MetricItem
|
||||
id={key}
|
||||
data={[metric]}
|
||||
isLoading={status === FETCH_STATUS.LOADING}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Chart, Metric, MetricDatum } from '@elastic/charts';
|
||||
import { EuiLoadingContent, EuiPanel } from '@elastic/eui';
|
||||
|
||||
export function MetricItem({
|
||||
data,
|
||||
id,
|
||||
isLoading,
|
||||
}: {
|
||||
data: MetricDatum[];
|
||||
id: number;
|
||||
isLoading: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
resize: 'none',
|
||||
padding: '0px',
|
||||
overflow: 'auto',
|
||||
height: '100px',
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<EuiPanel hasBorder={true}>
|
||||
<EuiLoadingContent lines={3} />
|
||||
</EuiPanel>
|
||||
) : (
|
||||
<Chart>
|
||||
<Metric id={`metric_${id}`} data={[data]} />
|
||||
</Chart>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -27,6 +27,7 @@ type MobileFiltersTypes =
|
|||
| 'appVersion'
|
||||
| 'osVersion'
|
||||
| 'netConnectionType';
|
||||
|
||||
type MobileFilters = Array<{
|
||||
key: MobileFiltersTypes;
|
||||
options: string[];
|
||||
|
|
146
x-pack/plugins/apm/server/routes/mobile/get_mobile_stats.ts
Normal file
146
x-pack/plugins/apm/server/routes/mobile/get_mobile_stats.ts
Normal file
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
termQuery,
|
||||
kqlQuery,
|
||||
rangeQuery,
|
||||
} from '@kbn/observability-plugin/server';
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
SESSION_ID,
|
||||
SPAN_SUBTYPE,
|
||||
APP_LAUNCH_TIME,
|
||||
EVENT_NAME,
|
||||
} from '../../../common/es_fields/apm';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { getBucketSize } from '../../lib/helpers/get_bucket_size';
|
||||
|
||||
type Timeseries = Array<{ x: number; y: number }>;
|
||||
export interface MobileStats {
|
||||
sessions: { value?: number; timeseries: Timeseries };
|
||||
requests: { value?: number | null; timeseries: Timeseries };
|
||||
maxLoadTime: { value?: number | null; timeseries: Timeseries };
|
||||
crashCount: { value?: number | null; timeseries: Timeseries };
|
||||
}
|
||||
|
||||
export async function getMobileStats({
|
||||
kuery,
|
||||
apmEventClient,
|
||||
serviceName,
|
||||
transactionType,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
kuery: string;
|
||||
apmEventClient: APMEventClient;
|
||||
serviceName: string;
|
||||
transactionType?: string;
|
||||
environment: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}): Promise<MobileStats> {
|
||||
const { intervalString } = getBucketSize({
|
||||
start,
|
||||
end,
|
||||
minBucketSize: 60,
|
||||
});
|
||||
|
||||
const aggs = {
|
||||
sessions: {
|
||||
cardinality: { field: SESSION_ID },
|
||||
},
|
||||
requests: {
|
||||
filter: { term: { [SPAN_SUBTYPE]: 'http' } },
|
||||
},
|
||||
maxLoadTime: {
|
||||
max: { field: APP_LAUNCH_TIME },
|
||||
},
|
||||
crashCount: {
|
||||
filter: { term: { [EVENT_NAME]: 'crash' } },
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search('get_mobile_stats', {
|
||||
apm: {
|
||||
events: [
|
||||
ProcessorEvent.error,
|
||||
ProcessorEvent.metric,
|
||||
ProcessorEvent.transaction,
|
||||
ProcessorEvent.span,
|
||||
],
|
||||
},
|
||||
body: {
|
||||
track_total_hits: false,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
...termQuery(TRANSACTION_TYPE, transactionType),
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
timeseries: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: intervalString,
|
||||
min_doc_count: 0,
|
||||
},
|
||||
aggs,
|
||||
},
|
||||
...aggs,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const durationAsMinutes = (end - start) / 1000 / 60;
|
||||
|
||||
return {
|
||||
sessions: {
|
||||
value: response.aggregations?.sessions?.value,
|
||||
timeseries:
|
||||
response.aggregations?.timeseries?.buckets.map((bucket) => ({
|
||||
x: bucket.key,
|
||||
y: bucket.sessions.value ?? 0,
|
||||
})) ?? [],
|
||||
},
|
||||
requests: {
|
||||
value: response.aggregations?.requests?.doc_count,
|
||||
timeseries:
|
||||
response.aggregations?.timeseries?.buckets.map((bucket) => ({
|
||||
x: bucket.key,
|
||||
y: bucket.requests.doc_count ?? 0,
|
||||
})) ?? [],
|
||||
},
|
||||
maxLoadTime: {
|
||||
value: response.aggregations?.maxLoadTime?.value,
|
||||
timeseries:
|
||||
response.aggregations?.timeseries?.buckets.map((bucket) => ({
|
||||
x: bucket.key,
|
||||
y: bucket.maxLoadTime?.value ?? 0,
|
||||
})) ?? [],
|
||||
},
|
||||
crashCount: {
|
||||
value:
|
||||
response.aggregations?.crashCount?.doc_count ?? 0 / durationAsMinutes,
|
||||
timeseries:
|
||||
response.aggregations?.timeseries?.buckets.map((bucket) => ({
|
||||
x: bucket.key,
|
||||
y: bucket.crashCount.doc_count ?? 0,
|
||||
})) ?? [],
|
||||
},
|
||||
};
|
||||
}
|
|
@ -10,6 +10,7 @@ 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';
|
||||
import { getMobileFilters } from './get_mobile_filters';
|
||||
import { getMobileStats, MobileStats } from './get_mobile_stats';
|
||||
|
||||
const mobileFilters = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/mobile/filters',
|
||||
|
@ -50,6 +51,43 @@ const mobileFilters = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const mobileStats = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/stats',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: t.intersection([
|
||||
kueryRt,
|
||||
rangeRt,
|
||||
environmentRt,
|
||||
t.partial({
|
||||
transactionType: t.string,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (resources): Promise<MobileStats> => {
|
||||
const apmEventClient = await getApmEventClient(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const { kuery, environment, start, end, transactionType } = params.query;
|
||||
|
||||
const stats = await getMobileStats({
|
||||
kuery,
|
||||
environment,
|
||||
transactionType,
|
||||
start,
|
||||
end,
|
||||
serviceName,
|
||||
apmEventClient,
|
||||
});
|
||||
|
||||
return stats;
|
||||
},
|
||||
});
|
||||
|
||||
export const mobileRouteRepository = {
|
||||
...mobileFilters,
|
||||
...mobileStats,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* 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';
|
||||
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
|
||||
export async function generateMobileData({
|
||||
start,
|
||||
end,
|
||||
synthtraceEsClient,
|
||||
}: {
|
||||
start: number;
|
||||
end: number;
|
||||
synthtraceEsClient: ApmSynthtraceEsClient;
|
||||
}) {
|
||||
const galaxy10 = apm
|
||||
.mobileApp({
|
||||
name: 'synth-android',
|
||||
environment: 'production',
|
||||
agentName: 'android/java',
|
||||
})
|
||||
.mobileDevice()
|
||||
.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: 'wifi' });
|
||||
|
||||
const galaxy7 = apm
|
||||
.mobileApp({
|
||||
name: 'synth-android',
|
||||
environment: 'production',
|
||||
agentName: 'android/java',
|
||||
})
|
||||
.mobileDevice()
|
||||
.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',
|
||||
});
|
||||
|
||||
return await synthtraceEsClient.index([
|
||||
timerange(start, end)
|
||||
.interval('5m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
galaxy10.startNewSession();
|
||||
galaxy7.startNewSession();
|
||||
return [
|
||||
galaxy10
|
||||
.transaction('Start View - View Appearing', 'Android Activity')
|
||||
.timestamp(timestamp)
|
||||
.duration(500)
|
||||
.success()
|
||||
.children(
|
||||
galaxy10
|
||||
.span({
|
||||
spanName: 'onCreate',
|
||||
spanType: 'app',
|
||||
spanSubtype: 'internal',
|
||||
})
|
||||
.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)
|
||||
),
|
||||
galaxy7
|
||||
.transaction('Start View - View Appearing', 'Android Activity')
|
||||
.timestamp(timestamp)
|
||||
.duration(20)
|
||||
.success()
|
||||
.children(
|
||||
galaxy7
|
||||
.span({
|
||||
spanName: 'onCreate',
|
||||
spanType: 'app',
|
||||
spanSubtype: 'internal',
|
||||
})
|
||||
.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,170 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
|
||||
import { sumBy } from 'lodash';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { generateMobileData } from './generate_mobile_data';
|
||||
|
||||
type MobileStats = APIReturnType<'GET /internal/apm/mobile-services/{serviceName}/stats'>;
|
||||
|
||||
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 getMobileStats({
|
||||
environment = ENVIRONMENT_ALL.value,
|
||||
kuery = '',
|
||||
serviceName,
|
||||
transactionType = 'mobile',
|
||||
}: {
|
||||
environment?: string;
|
||||
kuery?: string;
|
||||
serviceName: string;
|
||||
transactionType?: string;
|
||||
}) {
|
||||
return await apmApiClient
|
||||
.readUser({
|
||||
endpoint: 'GET /internal/apm/mobile-services/{serviceName}/stats',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
environment,
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
kuery,
|
||||
transactionType,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ body }) => body);
|
||||
}
|
||||
|
||||
registry.when('Mobile stats when data is not loaded', { config: 'basic', archives: [] }, () => {
|
||||
describe('when no data', () => {
|
||||
it('handles empty state', async () => {
|
||||
const response = await getMobileStats({ serviceName: 'foo' });
|
||||
expect(response).to.eql({
|
||||
sessions: {
|
||||
timeseries: [],
|
||||
},
|
||||
requests: {
|
||||
timeseries: [],
|
||||
},
|
||||
maxLoadTime: {
|
||||
timeseries: [],
|
||||
},
|
||||
crashCount: {
|
||||
value: 0,
|
||||
timeseries: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
registry.when('Mobile stats', { config: 'basic', archives: [] }, () => {
|
||||
before(async () => {
|
||||
await generateMobileData({
|
||||
synthtraceEsClient,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
describe('when data is loaded', () => {
|
||||
let response: MobileStats;
|
||||
|
||||
before(async () => {
|
||||
response = await getMobileStats({
|
||||
serviceName: 'synth-android',
|
||||
environment: 'production',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns same sessions', () => {
|
||||
const { value, timeseries } = response.sessions;
|
||||
const timeseriesTotal = sumBy(timeseries, 'y');
|
||||
expect(value).to.be(timeseriesTotal);
|
||||
});
|
||||
|
||||
it('returns same crashCount', () => {
|
||||
const { value, timeseries } = response.crashCount;
|
||||
const timeseriesTotal = sumBy(timeseries, 'y');
|
||||
expect(value).to.be(timeseriesTotal);
|
||||
});
|
||||
|
||||
it('returns same requests', () => {
|
||||
const { value, timeseries } = response.requests;
|
||||
const timeseriesTotal = sumBy(timeseries, 'y');
|
||||
expect(value).to.be(timeseriesTotal);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when filters are applied', () => {
|
||||
it('returns empty state for filters', async () => {
|
||||
const response = await getMobileStats({
|
||||
serviceName: 'synth-android',
|
||||
environment: 'production',
|
||||
kuery: `app.version:"none"`,
|
||||
});
|
||||
|
||||
expect(response).to.eql({
|
||||
sessions: {
|
||||
value: 0,
|
||||
timeseries: [],
|
||||
},
|
||||
requests: {
|
||||
value: 0,
|
||||
timeseries: [],
|
||||
},
|
||||
maxLoadTime: {
|
||||
value: null,
|
||||
timeseries: [],
|
||||
},
|
||||
crashCount: {
|
||||
value: 0,
|
||||
timeseries: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the correct values when single filter is applied', async () => {
|
||||
const response = await getMobileStats({
|
||||
serviceName: 'synth-android',
|
||||
environment: 'production',
|
||||
kuery: `network.connection.type:"wifi"`,
|
||||
});
|
||||
|
||||
expect(response.sessions.value).to.eql(3);
|
||||
expect(response.requests.value).to.eql(6);
|
||||
expect(response.crashCount.value).to.eql(0);
|
||||
expect(response.maxLoadTime.value).to.eql(null);
|
||||
});
|
||||
|
||||
it('returns the correct values when multiple filters are applied', async () => {
|
||||
const response = await getMobileStats({
|
||||
serviceName: 'synth-android',
|
||||
kuery: `app.version:"1.0" and environment: "production"`,
|
||||
});
|
||||
|
||||
expect(response.sessions.value).to.eql(0);
|
||||
expect(response.requests.value).to.eql(0);
|
||||
expect(response.crashCount.value).to.eql(0);
|
||||
expect(response.maxLoadTime.value).to.eql(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue