[APM] Introduce mobile top metrics (#148330)

## Summary

closes: #146854 




https://user-images.githubusercontent.com/3369346/211276601-92fb9d6e-60fd-4ca3-be37-66daac08d7ac.mov

![Screenshot 2023-01-10 at 16 20
59](https://user-images.githubusercontent.com/3369346/211756906-b3ded4dd-6fb3-489d-8165-e5fc1239e369.png)
![Screenshot 2023-01-10 at 16 17
58](https://user-images.githubusercontent.com/3369346/211756916-e8c57859-b8a6-4f9a-87d4-a72e27711485.png)



References for mobile fields (WIP)
- events
69a52f23d5/specs/agents/mobile/events.md (crashes)
- metrics
69a52f23d5/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:
Katerina Patticha 2023-01-12 09:37:35 +01:00 committed by GitHub
parent b05880e6ac
commit 61b8ef4363
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 719 additions and 1 deletions

View file

@ -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

View file

@ -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`;

View file

@ -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';

View file

@ -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}>

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -27,6 +27,7 @@ type MobileFiltersTypes =
| 'appVersion'
| 'osVersion'
| 'netConnectionType';
type MobileFilters = Array<{
key: MobileFiltersTypes;
options: string[];

View 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,
})) ?? [],
},
};
}

View file

@ -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,
};

View file

@ -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)
),
];
}),
]);
}

View file

@ -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);
});
});
});
}