Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[APM] migrate to io-ts #42961

Merged
merged 11 commits into from
Aug 21, 2019
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { dateAsStringRt } from './index';

describe('dateAsStringRt', () => {
it('validates whether a string is a valid date', () => {
expect(dateAsStringRt.decode(1566299881499).isLeft()).toBe(true);

expect(dateAsStringRt.decode('2019-08-20T11:18:31.407Z').isRight()).toBe(
true
);
});

it('returns the string it was given', () => {
const either = dateAsStringRt.decode('2019-08-20T11:18:31.407Z');

expect(either.value).toBe('2019-08-20T11:18:31.407Z');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { either } from 'fp-ts/lib/Either';

// Checks whether a string is a valid ISO timestamp,
// but doesn't convert it into a Date object when decoding

export const dateAsStringRt = new t.Type<string, string, unknown>(
'DateAsString',
t.string.is,
(input, context) =>
either.chain(t.string.validate(input, context), str => {
const date = new Date(str);
return isNaN(date.getTime()) ? t.failure(input, context) : t.success(str);
}),
t.identity
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import * as t from 'io-ts';
import { jsonRt } from './index';

describe('jsonRt', () => {
it('validates json', () => {
expect(jsonRt.decode('{}').isRight()).toBe(true);
expect(jsonRt.decode('[]').isRight()).toBe(true);
expect(jsonRt.decode('true').isRight()).toBe(true);
expect(jsonRt.decode({}).isLeft()).toBe(true);
expect(jsonRt.decode('foo').isLeft()).toBe(true);
});

it('returns parsed json when decoding', () => {
expect(jsonRt.decode('{}').value).toEqual({});
expect(jsonRt.decode('[]').value).toEqual([]);
expect(jsonRt.decode('true').value).toEqual(true);
});

it('is pipable', () => {
const piped = jsonRt.pipe(t.type({ foo: t.string }));

const validInput = { foo: 'bar' };
const invalidInput = { foo: null };

const valid = piped.decode(JSON.stringify(validInput));
const invalid = piped.decode(JSON.stringify(invalidInput));

expect(valid.isRight()).toBe(true);
expect(valid.value).toEqual(validInput);

expect(invalid.isLeft()).toBe(true);
});

it('returns strings when encoding', () => {
expect(jsonRt.encode({})).toBe('{}');
});
});
21 changes: 21 additions & 0 deletions x-pack/legacy/plugins/apm/common/runtime_types/json_rt/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as t from 'io-ts';
import { either } from 'fp-ts/lib/Either';

export const jsonRt = new t.Type<any, string, unknown>(
'JSON',
t.any.is,
(input, context) =>
either.chain(t.string.validate(input, context), str => {
try {
return t.success(JSON.parse(str));
} catch (e) {
return t.failure(input, context);
}
}),
a => JSON.stringify(a)
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { transactionSampleRateRt } from './index';

describe('transactionSampleRateRt', () => {
it('accepts both strings and numbers as values', () => {
expect(transactionSampleRateRt.decode('0.5').isRight()).toBe(true);
expect(transactionSampleRateRt.decode(0.5).isRight()).toBe(true);
});

it('checks if the number falls within 0, 1', () => {
expect(transactionSampleRateRt.decode(0).isRight()).toBe(true);

expect(transactionSampleRateRt.decode(0.5).isRight()).toBe(true);

expect(transactionSampleRateRt.decode(-0.1).isRight()).toBe(false);
expect(transactionSampleRateRt.decode(1.1).isRight()).toBe(false);

expect(transactionSampleRateRt.decode(NaN).isRight()).toBe(false);
});

it('checks whether the number of decimals is 3', () => {
expect(transactionSampleRateRt.decode(1).isRight()).toBe(true);
expect(transactionSampleRateRt.decode(0.99).isRight()).toBe(true);
expect(transactionSampleRateRt.decode(0.999).isRight()).toBe(true);
expect(transactionSampleRateRt.decode(0.998).isRight()).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import * as t from 'io-ts';

export const transactionSampleRateRt = new t.Type<number, number, unknown>(
'TransactionSampleRate',
t.number.is,
(input, context) => {
const value = Number(input);
return value >= 0 && value <= 1 && Number(value.toFixed(3)) === value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Number(value.toFixed(3)) === value looks so much nicer than the one I suggested 👍

? t.success(value)
: t.failure(input, context);
},
t.identity
);
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,14 @@ import styled from 'styled-components';
import { idx } from '@kbn/elastic-idx';
import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n';
import { useFetcher } from '../../../hooks/useFetcher';
import {
loadErrorDistribution,
loadErrorGroupDetails
} from '../../../services/rest/apm/error_groups';
import { fontFamilyCode, fontSizes, px, units } from '../../../style/variables';
import { ApmHeader } from '../../shared/ApmHeader';
import { DetailView } from './DetailView';
import { ErrorDistribution } from './Distribution';
import { useLocation } from '../../../hooks/useLocation';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { useTrackPageview } from '../../../../../infra/public';
import { callApmApi } from '../../../services/rest/callApmApi';

const Titles = styled.div`
margin-bottom: ${px(units.plus)};
Expand Down Expand Up @@ -68,24 +65,38 @@ export function ErrorGroupDetails() {

const { data: errorGroupData } = useFetcher(() => {
if (serviceName && start && end && errorGroupId) {
return loadErrorGroupDetails({
serviceName,
start,
end,
errorGroupId,
uiFilters
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/{groupId}',
params: {
path: {
serviceName,
groupId: errorGroupId
},
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters)
}
}
});
}
}, [serviceName, start, end, errorGroupId, uiFilters]);

const { data: errorDistributionData } = useFetcher(() => {
if (serviceName && start && end && errorGroupId) {
return loadErrorDistribution({
serviceName,
start,
end,
errorGroupId,
uiFilters
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/distribution',
params: {
path: {
serviceName
},
query: {
start,
end,
groupId: errorGroupId,
uiFilters: JSON.stringify(uiFilters)
}
}
});
}
}, [serviceName, start, end, errorGroupId, uiFilters]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,13 @@ import {
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import { useFetcher } from '../../../hooks/useFetcher';
import {
loadErrorDistribution,
loadErrorGroupList
} from '../../../services/rest/apm/error_groups';
import { ErrorDistribution } from '../ErrorGroupDetails/Distribution';
import { ErrorGroupList } from './List';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { useTrackPageview } from '../../../../../infra/public';
import { PROJECTION } from '../../../../common/projections/typings';
import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { callApmApi } from '../../../services/rest/callApmApi';

const ErrorGroupOverview: React.SFC = () => {
const { urlParams, uiFilters } = useUrlParams();
Expand All @@ -32,24 +29,38 @@ const ErrorGroupOverview: React.SFC = () => {

const { data: errorDistributionData } = useFetcher(() => {
if (serviceName && start && end) {
return loadErrorDistribution({
serviceName,
start,
end,
uiFilters
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/distribution',
params: {
path: {
serviceName
},
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters)
}
}
});
}
}, [serviceName, start, end, uiFilters]);

const { data: errorGroupListData } = useFetcher(() => {
if (serviceName && start && end) {
return loadErrorGroupList({
serviceName,
start,
end,
sortField,
sortDirection,
uiFilters
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors',
params: {
path: {
serviceName
},
query: {
start,
end,
sortField,
sortDirection,
uiFilters: JSON.stringify(uiFilters)
}
}
});
}
}, [serviceName, start, end, sortField, sortDirection, uiFilters]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { ErrorGroupOverview } from '../ErrorGroupOverview';
import { TransactionOverview } from '../TransactionOverview';
import { ServiceMetrics } from '../ServiceMetrics';
import { useFetcher } from '../../../hooks/useFetcher';
import { loadServiceAgentName } from '../../../services/rest/apm/services';
import { isRumAgentName } from '../../../../common/agent_name';
import { callApmApi } from '../../../services/rest/callApmApi';

interface Props {
urlParams: IUrlParams;
Expand All @@ -23,7 +23,13 @@ export function ServiceDetailTabs({ urlParams }: Props) {
const { serviceName, start, end } = urlParams;
const { data: agentName } = useFetcher(() => {
if (serviceName && start && end) {
return loadServiceAgentName({ serviceName, start, end });
return callApmApi({
pathname: '/api/apm/services/{serviceName}/agent_name',
params: {
path: { serviceName },
query: { start, end }
}
}).then(res => res.agentName);
}
}, [serviceName, start, end]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import React from 'react';
import { render, wait, waitForElement } from 'react-testing-library';
import 'react-testing-library/cleanup-after-each';
import { toastNotifications } from 'ui/notify';
import * as apmRestServices from '../../../../services/rest/apm/services';
import * as callApmApi from '../../../../services/rest/callApmApi';
import { ServiceOverview } from '..';
import * as urlParamsHooks from '../../../../hooks/useUrlParams';
import * as coreHooks from '../../../../hooks/useCore';
Expand Down Expand Up @@ -66,7 +66,7 @@ describe('Service Overview -> View', () => {
it('should render services, when list is not empty', async () => {
// mock rest requests
const dataFetchingSpy = jest
.spyOn(apmRestServices, 'loadServiceList')
.spyOn(callApmApi, 'callApmApi')
.mockResolvedValue({
hasLegacyData: false,
hasHistoricalData: true,
Expand Down Expand Up @@ -101,7 +101,7 @@ describe('Service Overview -> View', () => {

it('should render getting started message, when list is empty and no historical data is found', async () => {
const dataFetchingSpy = jest
.spyOn(apmRestServices, 'loadServiceList')
.spyOn(callApmApi, 'callApmApi')
.mockResolvedValue({
hasLegacyData: false,
hasHistoricalData: false,
Expand All @@ -125,7 +125,7 @@ describe('Service Overview -> View', () => {

it('should render empty message, when list is empty and historical data is found', async () => {
const dataFetchingSpy = jest
.spyOn(apmRestServices, 'loadServiceList')
.spyOn(callApmApi, 'callApmApi')
.mockResolvedValue({
hasLegacyData: false,
hasHistoricalData: true,
Expand All @@ -145,7 +145,7 @@ describe('Service Overview -> View', () => {
// create spies
const toastSpy = jest.spyOn(toastNotifications, 'addWarning');
const dataFetchingSpy = jest
.spyOn(apmRestServices, 'loadServiceList')
.spyOn(callApmApi, 'callApmApi')
.mockResolvedValue({
hasLegacyData: true,
hasHistoricalData: true,
Expand All @@ -168,7 +168,7 @@ describe('Service Overview -> View', () => {
// create spies
const toastSpy = jest.spyOn(toastNotifications, 'addWarning');
const dataFetchingSpy = jest
.spyOn(apmRestServices, 'loadServiceList')
.spyOn(callApmApi, 'callApmApi')
.mockResolvedValue({
hasLegacyData: false,
hasHistoricalData: true,
Expand Down
Loading