From 126948cfed61a230d9aff30d0234462ed307b372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 21 Nov 2018 19:35:26 +0100 Subject: [PATCH] [APM] Move ML anomaly transformation to backend --- .../shared/charts/CustomPlot/StaticPlot.js | 8 +- .../plugins/apm/public/services/rest/apm.ts | 2 +- .../transactionDetailsCharts.tsx | 2 +- .../transactionOverviewCharts.tsx | 11 +- .../__tests__/chartSelectors.test.ts | 82 +----- .../public/store/selectors/chartSelectors.ts | 119 +------- .../__snapshots__/fetcher.test.ts.snap | 26 +- .../__snapshots__/index.test.ts.snap | 54 ++++ .../__snapshots__/transform.test.ts.snap | 49 ++++ .../fetcher.test.ts | 19 +- .../fetcher.ts | 64 ++--- .../get_anomaly_series/get_ml_bucket_size.ts | 64 +++++ .../charts/get_anomaly_series/index.test.ts | 60 ++++ .../charts/get_anomaly_series/index.ts | 54 ++++ .../mock-responses/mlAnomalyResponse.ts} | 70 ++--- .../mock-responses/mlBucketSpanResponse.ts | 32 +++ .../get_anomaly_series/transform.test.ts | 272 ++++++++++++++++++ .../charts/get_anomaly_series/transform.ts | 172 +++++++++++ ...s_with_initial_anomaly_bounds.test.ts.snap | 46 --- .../__snapshots__/transform.test.ts.snap | 49 ---- .../get_anomaly_aggs/index.ts | 13 - .../get_anomaly_aggs/transform.test.ts | 18 -- .../get_anomaly_aggs/transform.ts | 37 --- ...uckets_with_initial_anomaly_bounds.test.ts | 55 ---- ...get_buckets_with_initial_anomaly_bounds.ts | 66 ----- .../index.test.ts | 55 ---- .../get_avg_response_time_anomalies/index.ts | 60 ---- .../mock-responses/firstBucketsResponse.ts | 89 ------ .../__snapshots__/transform.test.ts.snap | 1 - .../charts/get_timeseries_data/fetcher.ts | 9 +- .../charts/get_timeseries_data/index.ts | 29 +- .../get_timeseries_data/transform.test.ts | 8 +- .../charts/get_timeseries_data/transform.ts | 11 +- 33 files changed, 877 insertions(+), 829 deletions(-) rename x-pack/plugins/apm/server/lib/transactions/charts/{get_avg_response_time_anomalies/get_anomaly_aggs => get_anomaly_series}/__snapshots__/fetcher.test.ts.snap (78%) create mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/__snapshots__/index.test.ts.snap create mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/__snapshots__/transform.test.ts.snap rename x-pack/plugins/apm/server/lib/transactions/charts/{get_avg_response_time_anomalies/get_anomaly_aggs => get_anomaly_series}/fetcher.test.ts (75%) rename x-pack/plugins/apm/server/lib/transactions/charts/{get_avg_response_time_anomalies/get_anomaly_aggs => get_anomaly_series}/fetcher.ts (65%) create mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/get_ml_bucket_size.ts create mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/index.test.ts create mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/index.ts rename x-pack/plugins/apm/server/lib/transactions/charts/{get_avg_response_time_anomalies/mock-responses/mainBucketsResponse.ts => get_anomaly_series/mock-responses/mlAnomalyResponse.ts} (63%) create mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/mock-responses/mlBucketSpanResponse.ts create mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/transform.test.ts create mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/transform.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/__snapshots__/get_buckets_with_initial_anomaly_bounds.test.ts.snap delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/__snapshots__/transform.test.ts.snap delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/index.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/transform.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/transform.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_buckets_with_initial_anomaly_bounds.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_buckets_with_initial_anomaly_bounds.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/index.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/index.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/mock-responses/firstBucketsResponse.ts diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js index 11b0db13e9d64d4..a033c47883261ed 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js @@ -9,7 +9,8 @@ import { YAxis, HorizontalGridLines, LineSeries, - AreaSeries + AreaSeries, + VerticalRectSeries } from 'react-vis'; import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; @@ -56,13 +57,14 @@ class StaticPlot extends PureComponent { case 'areaMaxHeight': const yMax = last(plotValues.yTickValues); const data = serie.data.map(p => ({ + x0: p.x0, x: p.x, y0: 0, - y: p.y ? yMax : null + y: yMax })); return ( - d.y !== null} key={serie.title} xType="time" diff --git a/x-pack/plugins/apm/public/services/rest/apm.ts b/x-pack/plugins/apm/public/services/rest/apm.ts index 692c360980ef828..c85ea7f8c1a5bd7 100644 --- a/x-pack/plugins/apm/public/services/rest/apm.ts +++ b/x-pack/plugins/apm/public/services/rest/apm.ts @@ -10,7 +10,7 @@ import { ServiceAPIResponse } from 'x-pack/plugins/apm/server/lib/services/get_s import { ServiceListAPIResponse } from 'x-pack/plugins/apm/server/lib/services/get_services'; import { TraceListAPIResponse } from 'x-pack/plugins/apm/server/lib/traces/get_top_traces'; import { TraceAPIResponse } from 'x-pack/plugins/apm/server/lib/traces/get_trace'; -import { TimeSeriesAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform'; +import { TimeSeriesAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data'; import { ITransactionDistributionAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/distribution'; import { TransactionListAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/get_top_transactions'; import { TransactionAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/get_transaction'; diff --git a/x-pack/plugins/apm/public/store/reactReduxRequest/transactionDetailsCharts.tsx b/x-pack/plugins/apm/public/store/reactReduxRequest/transactionDetailsCharts.tsx index 45c0d3e9f53d193..01f3f0bd780ab0e 100644 --- a/x-pack/plugins/apm/public/store/reactReduxRequest/transactionDetailsCharts.tsx +++ b/x-pack/plugins/apm/public/store/reactReduxRequest/transactionDetailsCharts.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { Request, RRRRender } from 'react-redux-request'; import { createSelector } from 'reselect'; -import { TimeSeriesAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform'; +import { TimeSeriesAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data'; import { loadCharts } from '../../services/rest/apm'; import { IReduxState } from '../rootReducer'; import { getCharts } from '../selectors/chartSelectors'; diff --git a/x-pack/plugins/apm/public/store/reactReduxRequest/transactionOverviewCharts.tsx b/x-pack/plugins/apm/public/store/reactReduxRequest/transactionOverviewCharts.tsx index 87ecf58e0e33885..9cb5760140e2870 100644 --- a/x-pack/plugins/apm/public/store/reactReduxRequest/transactionOverviewCharts.tsx +++ b/x-pack/plugins/apm/public/store/reactReduxRequest/transactionOverviewCharts.tsx @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, isEmpty } from 'lodash'; +import { get } from 'lodash'; import React from 'react'; import { Request, RRRRender } from 'react-redux-request'; import { createSelector } from 'reselect'; -import { TimeSeriesAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform'; +import { TimeSeriesAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data'; import { loadCharts } from '../../services/rest/apm'; import { IReduxState } from '../rootReducer'; import { getCharts } from '../selectors/chartSelectors'; @@ -39,11 +39,8 @@ export const getTransactionOverviewCharts = createSelector( ); export function hasDynamicBaseline(state: IReduxState) { - return !isEmpty( - get( - state, - `reactReduxRequest[${ID}].data.responseTimes.avgAnomalies.buckets` - ) + return ( + get(state, `reactReduxRequest[${ID}].data.anomalyTimeSeries`) !== undefined ); } diff --git a/x-pack/plugins/apm/public/store/selectors/__tests__/chartSelectors.test.ts b/x-pack/plugins/apm/public/store/selectors/__tests__/chartSelectors.test.ts index 608a87ee48f2c69..b660d30a326abb1 100644 --- a/x-pack/plugins/apm/public/store/selectors/__tests__/chartSelectors.test.ts +++ b/x-pack/plugins/apm/public/store/selectors/__tests__/chartSelectors.test.ts @@ -4,55 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AvgAnomalyBucket } from 'x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/transform'; -import { TimeSeriesAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform'; -import { - getAnomalyBoundaryValues, - getAnomalyScoreValues, - getResponseTimeSeries, - getTpmSeries -} from '../chartSelectors'; -import { anomalyData } from './mockData/anomalyData'; +import { TimeSeriesAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data'; +import { getResponseTimeSeries, getTpmSeries } from '../chartSelectors'; describe('chartSelectors', () => { - describe('getAnomalyScoreValues', () => { - it('should return anomaly score series', () => { - const dates = [0, 1000, 2000, 3000, 4000, 5000, 6000]; - const buckets = [ - { - anomalyScore: null - }, - { - anomalyScore: 80 - }, - { - anomalyScore: 0 - }, - { - anomalyScore: 0 - }, - { - anomalyScore: 70 - }, - { - anomalyScore: 80 - }, - { - anomalyScore: 0 - } - ] as AvgAnomalyBucket[]; - - expect(getAnomalyScoreValues(dates, buckets, 1000)).toEqual([ - { x: 1000, y: 1 }, - { x: 2000, y: 1 }, - { x: 3000 }, - { x: 5000, y: 1 }, - { x: 6000, y: 1 }, - { x: 7000 } - ]); - }); - }); - describe('getResponseTimeSeries', () => { const chartsData = { dates: [0, 1000, 2000, 3000, 4000, 5000], @@ -101,37 +56,4 @@ describe('chartSelectors', () => { expect(getTpmSeries(chartsData, transactionType)).toMatchSnapshot(); }); }); - - describe('getAnomalyBoundaryValues', () => { - const { dates, buckets } = anomalyData; - const bucketSize = 240000; - - it('should return correct buckets', () => { - expect(getAnomalyBoundaryValues(dates, buckets, bucketSize)).toEqual([ - { x: 1530614880000, y: 54799, y0: 15669 }, - { x: 1530615060000, y: 49874, y0: 17808 }, - { x: 1530615300000, y: 49421, y0: 18012 }, - { x: 1530615540000, y: 49654, y0: 17889 }, - { x: 1530615780000, y: 50026, y0: 17713 }, - { x: 1530616020000, y: 49371, y0: 18044 }, - { x: 1530616260000, y: 50110, y0: 17713 }, - { x: 1530616500000, y: 50419, y0: 17582 }, - { x: 1530616620000, y: 50419, y0: 17582 } - ]); - }); - - it('should extend the last bucket with a size of bucketSize', () => { - const [lastBucket, secondLastBuckets] = getAnomalyBoundaryValues( - dates, - buckets, - bucketSize - ).reverse(); - - expect(secondLastBuckets.y).toBe(lastBucket.y); - expect(secondLastBuckets.y0).toBe(lastBucket.y0); - expect( - (lastBucket.x as number) - (secondLastBuckets.x as number) - ).toBeLessThanOrEqual(bucketSize); - }); - }); }); diff --git a/x-pack/plugins/apm/public/store/selectors/chartSelectors.ts b/x-pack/plugins/apm/public/store/selectors/chartSelectors.ts index 2c84398e0548e16..a5d5384f72a35eb 100644 --- a/x-pack/plugins/apm/public/store/selectors/chartSelectors.ts +++ b/x-pack/plugins/apm/public/store/selectors/chartSelectors.ts @@ -5,11 +5,9 @@ */ import d3 from 'd3'; -import { difference, last, memoize, zipObject } from 'lodash'; -import { rgba } from 'polished'; +import { difference, memoize, zipObject } from 'lodash'; import { colors } from 'x-pack/plugins/apm/common/variables'; -import { AvgAnomalyBucket } from 'x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/transform'; -import { TimeSeriesAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform'; +import { TimeSeriesAPIResponse } from 'x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data'; import { StringMap } from 'x-pack/plugins/apm/typings/common'; import { asDecimal, asMillis, tpmUnit } from '../../utils/formatters'; import { IUrlParams } from '../urlParams'; @@ -19,10 +17,6 @@ interface Coordinate { y?: number | null; } -interface BoundaryCoordinate extends Coordinate { - y0: number | null; -} - export const getEmptySerie = memoize( (start = Date.now() - 3600000, end = Date.now()) => { const dates = d3.time @@ -76,8 +70,8 @@ interface TimeSerie { } export function getResponseTimeSeries(chartsData: TimeSeriesAPIResponse) { - const { dates, overallAvgDuration } = chartsData; - const { avg, p95, p99, avgAnomalies } = chartsData.responseTimes; + const { dates, overallAvgDuration, anomalyTimeSeries } = chartsData; + const { avg, p95, p99 } = chartsData.responseTimes; const series: TimeSerie[] = [ { @@ -103,35 +97,10 @@ export function getResponseTimeSeries(chartsData: TimeSeriesAPIResponse) { } ]; - if (avgAnomalies) { - // insert after Avg. serie - series.splice(1, 0, { - title: 'Anomaly Boundaries', - hideLegend: true, - hideTooltipValue: true, - data: getAnomalyBoundaryValues( - dates, - avgAnomalies.buckets, - avgAnomalies.bucketSizeAsMillis - ), - type: 'area', - color: 'none', - areaColor: rgba(colors.apmBlue, 0.1) - }); - - series.splice(1, 0, { - title: 'Anomaly score', - hideLegend: true, - hideTooltipValue: true, - data: getAnomalyScoreValues( - dates, - avgAnomalies.buckets, - avgAnomalies.bucketSizeAsMillis - ), - type: 'areaMaxHeight', - color: 'none', - areaColor: rgba(colors.apmRed, 0.1) - }); + if (anomalyTimeSeries) { + // insert after Avg. series + series.splice(1, 0, anomalyTimeSeries.anomalyBoundariesSeries); + series.splice(1, 0, anomalyTimeSeries.anomalyScoreSeries); } return series; @@ -194,75 +163,3 @@ function getChartValues( y: buckets[i] })); } - -export function getAnomalyScoreValues( - dates: number[] = [], - buckets: AvgAnomalyBucket[] = [], - bucketSizeAsMillis: number -) { - const ANOMALY_THRESHOLD = 75; - const getX = (currentX: number, i: number) => - currentX + bucketSizeAsMillis * i; - - return dates - .map((date, i) => { - const { anomalyScore } = buckets[i]; - return { - x: date, - anomalyScore - }; - }) - .filter(p => { - const res = - p && p.anomalyScore != null && p.anomalyScore > ANOMALY_THRESHOLD; - return res; - }) - .reduce((acc, p, i, points) => { - const nextPoint = points[i + 1] || {}; - const endX = getX(p.x, 1); - acc.push({ x: p.x, y: 1 }); - if (nextPoint.x == null || nextPoint.x > endX) { - acc.push( - { - x: endX, - y: 1 - }, - { - x: getX(p.x, 2) - } - ); - } - - return acc; - }, []); -} - -export function getAnomalyBoundaryValues( - dates: number[] = [], - buckets: AvgAnomalyBucket[] = [], - bucketSizeAsMillis: number -) { - const lastX = last(dates); - return dates - .map((date, i) => { - const bucket = buckets[i]; - return { - x: date, - y0: bucket.lower, - y: bucket.upper - }; - }) - .filter(p => p.y != null) - .reduce((acc, p, i, points) => { - const isLast = last(points) === p; - acc.push(p); - - if (isLast) { - acc.push({ - ...p, - x: Math.min(p.x + bucketSizeAsMillis, lastX) // avoid going beyond the last date - }); - } - return acc; - }, []); -} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/__snapshots__/fetcher.test.ts.snap similarity index 78% rename from x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/__snapshots__/fetcher.test.ts.snap rename to x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/__snapshots__/fetcher.test.ts.snap index f3436641f3f1aab..81cec8a3a7a300c 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/__snapshots__/fetcher.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/__snapshots__/fetcher.test.ts.snap @@ -27,27 +27,14 @@ Array [ }, "date_histogram": Object { "extended_bounds": Object { - "max": 1, - "min": 0, + "max": 200000, + "min": 90000, }, "field": "timestamp", "interval": "myInterval", "min_doc_count": 0, }, }, - "top_hits": Object { - "top_hits": Object { - "_source": Object { - "includes": Array [ - "bucket_span", - ], - }, - "size": 1, - "sort": Array [ - "bucket_span", - ], - }, - }, }, "query": Object { "bool": Object { @@ -56,12 +43,17 @@ Array [ "range": Object { "timestamp": Object { "format": "epoch_millis", - "gte": 0, - "lte": 1, + "gte": 90000, + "lte": 200000, }, }, }, ], + "must": Object { + "exists": Object { + "field": "bucket_span", + }, + }, }, }, "size": 0, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/__snapshots__/index.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/__snapshots__/index.test.ts.snap new file mode 100644 index 000000000000000..ada00d62b0ca616 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/__snapshots__/index.test.ts.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getAnomalySeries should match snapshot 1`] = ` +Object { + "anomalyBoundariesSeries": Object { + "areaColor": "rgba(49,133,252,0.1)", + "color": "none", + "data": Array [ + Object { + "x": 5000, + "y": 200, + "y0": 20, + }, + Object { + "x": 15000, + "y": 100, + "y0": 20, + }, + Object { + "x": 25000, + "y": 50, + "y0": 10, + }, + Object { + "x": 30000, + "y": 50, + "y0": 10, + }, + ], + "hideLegend": true, + "hideTooltipValue": true, + "title": "Anomaly Boundaries", + "type": "area", + }, + "anomalyScoreSeries": Object { + "areaColor": "rgba(146,0,0,0.1)", + "color": "none", + "data": Array [ + Object { + "x": 25000, + "x0": 15000, + }, + Object { + "x": 35000, + "x0": 25000, + }, + ], + "hideLegend": true, + "hideTooltipValue": true, + "title": "Anomaly score", + "type": "areaMaxHeight", + }, +} +`; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/__snapshots__/transform.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/__snapshots__/transform.test.ts.snap new file mode 100644 index 000000000000000..31f049d034b43c0 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/__snapshots__/transform.test.ts.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`anomalySeriesTransform should match snapshot 1`] = ` +Object { + "anomalyBoundariesSeries": Object { + "areaColor": "rgba(49,133,252,0.1)", + "color": "none", + "data": Array [ + Object { + "x": 10000, + "y": 200, + "y0": 20, + }, + Object { + "x": 15000, + "y": 100, + "y0": 20, + }, + Object { + "x": 25000, + "y": 50, + "y0": 10, + }, + ], + "hideLegend": true, + "hideTooltipValue": true, + "title": "Anomaly Boundaries", + "type": "area", + }, + "anomalyScoreSeries": Object { + "areaColor": "rgba(146,0,0,0.1)", + "color": "none", + "data": Array [ + Object { + "x": 25000, + "x0": 15000, + }, + Object { + "x": 35000, + "x0": 25000, + }, + ], + "hideLegend": true, + "hideTooltipValue": true, + "title": "Anomaly score", + "type": "areaMaxHeight", + }, +} +`; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/fetcher.test.ts similarity index 75% rename from x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/fetcher.test.ts rename to x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/fetcher.test.ts index 7eaf84be12e79cf..be6460101d6d374 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/fetcher.test.ts @@ -4,22 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { anomalyAggsFetcher, ESResponse } from './fetcher'; +import { anomalySeriesFetcher, ESResponse } from './fetcher'; describe('anomalyAggsFetcher', () => { describe('when ES returns valid response', () => { - let response: ESResponse; + let response: ESResponse | undefined; let clientSpy: jest.Mock; beforeEach(async () => { clientSpy = jest.fn().mockReturnValue('ES Response'); - response = await anomalyAggsFetcher({ + response = await anomalySeriesFetcher({ serviceName: 'myServiceName', transactionType: 'myTransactionType', intervalString: 'myInterval', - client: clientSpy, - start: 0, - end: 1 + mlBucketSize: 10, + setup: { client: clientSpy, start: 100000, end: 200000 } as any }); }); @@ -38,8 +37,8 @@ describe('anomalyAggsFetcher', () => { const failClient = jest.fn(() => Promise.reject(httpError)); return expect( - anomalyAggsFetcher({ client: failClient } as any) - ).resolves.toEqual(null); + anomalySeriesFetcher({ setup: { client: failClient } } as any) + ).resolves.toEqual(undefined); }); it('should throw other errors', () => { @@ -47,8 +46,8 @@ describe('anomalyAggsFetcher', () => { const failClient = jest.fn(() => Promise.reject(otherError)); return expect( - anomalyAggsFetcher({ - client: failClient + anomalySeriesFetcher({ + setup: { client: failClient } } as any) ).rejects.toThrow(otherError); }); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/fetcher.ts similarity index 65% rename from x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/fetcher.ts rename to x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/fetcher.ts index eeb1fc32b8f6853..c9369e616b2a247 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/fetcher.ts @@ -5,21 +5,11 @@ */ import { AggregationSearchResponse } from 'elasticsearch'; -import { TopHits } from 'x-pack/plugins/apm/typings/elasticsearch'; -import { ESClient } from '../../../../helpers/setup_request'; +import { Setup } from '../../../helpers/setup_request'; -export interface IOptions { - serviceName: string; - transactionType: string; - intervalString: string; - client: ESClient; - start: number; - end: number; -} - -interface Bucket { - key_as_string: string; - key: number; +export interface ESBucket { + key_as_string: string; // timestamp as string + key: number; // timestamp doc_count: number; anomaly_score: { value: number | null; @@ -34,34 +24,47 @@ interface Bucket { interface Aggs { ml_avg_response_times: { - buckets: Bucket[]; + buckets: ESBucket[]; }; - top_hits: TopHits<{ - bucket_span: number; - }>; } -export type ESResponse = AggregationSearchResponse | null; +export type ESResponse = AggregationSearchResponse; -export async function anomalyAggsFetcher({ +export async function anomalySeriesFetcher({ serviceName, transactionType, intervalString, - client, - start, - end -}: IOptions): Promise { + mlBucketSize, + setup +}: { + serviceName: string; + transactionType: string; + intervalString: string; + mlBucketSize: number; + setup: Setup; +}) { + const { client, start, end } = setup; + + // move the start back with one bucket size, to ensure to get anomaly data in the beginning + // this is required because ML has a minimum bucket size (default is 900s) so if our buckets are smaller, we might have several null buckets in the beginning + const newStart = start - mlBucketSize * 1000; + const params = { index: `.ml-anomalies-${serviceName}-${transactionType}-high_mean_response_time`.toLowerCase(), body: { size: 0, query: { bool: { + must: { + exists: { + field: 'bucket_span' + } + }, filter: [ { range: { timestamp: { - gte: start, + gte: newStart, lte: end, format: 'epoch_millis' } @@ -71,20 +74,13 @@ export async function anomalyAggsFetcher({ } }, aggs: { - top_hits: { - top_hits: { - sort: ['bucket_span'], - _source: { includes: ['bucket_span'] }, - size: 1 - } - }, ml_avg_response_times: { date_histogram: { field: 'timestamp', interval: intervalString, min_doc_count: 0, extended_bounds: { - min: start, + min: newStart, max: end } }, @@ -103,7 +99,7 @@ export async function anomalyAggsFetcher({ } catch (err) { const isHttpError = 'statusCode' in err; if (isHttpError) { - return null; + return; } throw err; } diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/get_ml_bucket_size.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/get_ml_bucket_size.ts new file mode 100644 index 000000000000000..4a7ffe14ec492bc --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/get_ml_bucket_size.ts @@ -0,0 +1,64 @@ +/* + * 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 { oc } from 'ts-optchain'; +import { Setup } from '../../../helpers/setup_request'; + +interface IOptions { + serviceName: string; + transactionType: string; + setup: Setup; +} + +interface ESResponse { + bucket_span: number; +} + +export async function getMlBucketSize({ + serviceName, + transactionType, + setup +}: IOptions): Promise { + const { client, start, end } = setup; + const params = { + index: `.ml-anomalies-${serviceName}-${transactionType}-high_mean_response_time`.toLowerCase(), + body: { + _source: 'bucket_span', + size: 1, + query: { + bool: { + must: { + exists: { + field: 'bucket_span' + } + }, + filter: [ + { + range: { + timestamp: { + gte: start, + lte: end, + format: 'epoch_millis' + } + } + } + ] + } + } + } + }; + + try { + const resp = await client('search', params); + return oc(resp).hits.hits[0]._source.bucket_span(0); + } catch (err) { + const isHttpError = 'statusCode' in err; + if (isHttpError) { + return 0; + } + throw err; + } +} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/index.test.ts new file mode 100644 index 000000000000000..086e19f06da1536 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/index.test.ts @@ -0,0 +1,60 @@ +/* + * 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 { getAnomalySeries } from '.'; +import { bucketTransformer } from '../../distribution/get_buckets/transform'; +import { mlAnomalyResponse } from './mock-responses/mlAnomalyResponse'; +import { mlBucketSpanResponse } from './mock-responses/mlBucketSpanResponse'; +import { AnomalyTimeSeriesResponse } from './transform'; + +describe('getAnomalySeries', () => { + let avgAnomalies: AnomalyTimeSeriesResponse; + beforeEach(async () => { + const clientSpy = jest + .fn() + .mockResolvedValueOnce(mlBucketSpanResponse) + .mockResolvedValueOnce(mlAnomalyResponse); + + avgAnomalies = (await getAnomalySeries({ + serviceName: 'myServiceName', + transactionType: 'myTransactionType', + timeSeriesDates: [100, 100000], + setup: { + start: 0, + end: 500000, + client: clientSpy, + config: { + get: () => 'myIndex' as any + } + } + })) as AnomalyTimeSeriesResponse; + }); + + it('should remove buckets lower than threshold and outside date range from anomalyScoreSeries', () => { + expect(avgAnomalies.anomalyScoreSeries.data).toEqual([ + { x0: 15000, x: 25000 }, + { x0: 25000, x: 35000 } + ]); + }); + + it('should remove buckets outside date range from anomalyBoundariesSeries', () => { + expect( + avgAnomalies.anomalyBoundariesSeries.data.filter( + bucket => bucket.x < 100 || bucket.x > 100000 + ).length + ).toBe(0); + }); + + it('should remove buckets with null from anomalyBoundariesSeries', () => { + expect( + avgAnomalies.anomalyBoundariesSeries.data.filter(p => p.y === null).length + ).toBe(0); + }); + + it('should match snapshot', async () => { + expect(avgAnomalies).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/index.ts new file mode 100644 index 000000000000000..517df382cf89682 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/index.ts @@ -0,0 +1,54 @@ +/* + * 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 { getBucketSize } from '../../../helpers/get_bucket_size'; +import { Setup } from '../../../helpers/setup_request'; +import { anomalySeriesFetcher } from './fetcher'; +import { getMlBucketSize } from './get_ml_bucket_size'; +import { anomalySeriesTransform } from './transform'; + +export async function getAnomalySeries({ + serviceName, + transactionType, + transactionName, + timeSeriesDates, + setup +}: { + serviceName: string; + transactionType: string; + transactionName?: string; + timeSeriesDates: number[]; + setup: Setup; +}) { + // don't fetch anomalies for transaction details page + if (transactionName) { + return; + } + + const mlBucketSize = await getMlBucketSize({ + serviceName, + transactionType, + setup + }); + + const { start, end } = setup; + const { intervalString, bucketSize } = getBucketSize(start, end, 'auto'); + + const esResponse = await anomalySeriesFetcher({ + serviceName, + transactionType, + intervalString, + mlBucketSize, + setup + }); + + return anomalySeriesTransform( + esResponse, + mlBucketSize, + bucketSize, + timeSeriesDates + ); +} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/mock-responses/mainBucketsResponse.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/mock-responses/mlAnomalyResponse.ts similarity index 63% rename from x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/mock-responses/mainBucketsResponse.ts rename to x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/mock-responses/mlAnomalyResponse.ts index 21983541af24b7c..eaea107e5ef2db2 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/mock-responses/mainBucketsResponse.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/mock-responses/mlAnomalyResponse.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESResponse } from '../get_anomaly_aggs/fetcher'; +import { ESResponse } from '../fetcher'; -export const mainBucketsResponse: ESResponse = { +export const mlAnomalyResponse: ESResponse = { took: 3, timed_out: false, _shards: { @@ -25,21 +25,21 @@ export const mainBucketsResponse: ESResponse = { buckets: [ { key_as_string: '2018-07-02T09:16:40.000Z', - key: 1530523000000, + key: 0, doc_count: 0, anomaly_score: { value: null }, upper: { - value: null + value: 200 }, lower: { - value: null + value: 20 } }, { key_as_string: '2018-07-02T09:25:00.000Z', - key: 1530523500000, + key: 5000, doc_count: 4, anomaly_score: { value: null @@ -53,7 +53,7 @@ export const mainBucketsResponse: ESResponse = { }, { key_as_string: '2018-07-02T09:33:20.000Z', - key: 1530524000000, + key: 10000, doc_count: 0, anomaly_score: { value: null @@ -67,21 +67,21 @@ export const mainBucketsResponse: ESResponse = { }, { key_as_string: '2018-07-02T09:41:40.000Z', - key: 1530524500000, + key: 15000, doc_count: 2, anomaly_score: { - value: 0 + value: 90 }, upper: { - value: 54158.77731018045 + value: 100 }, lower: { - value: 16034.081569306454 + value: 20 } }, { key_as_string: '2018-07-02T09:50:00.000Z', - key: 1530525000000, + key: 20000, doc_count: 0, anomaly_score: { value: null @@ -95,65 +95,33 @@ export const mainBucketsResponse: ESResponse = { }, { key_as_string: '2018-07-02T09:58:20.000Z', - key: 1530525500000, + key: 25000, doc_count: 2, anomaly_score: { - value: 0 + value: 100 }, upper: { - value: 54158.77731018045 + value: 50 }, lower: { - value: 16034.081569306454 - } - }, - { - key_as_string: '2018-07-02T10:06:40.000Z', - key: 1530526000000, - doc_count: 0, - anomaly_score: { - value: null - }, - upper: { - value: null - }, - lower: { - value: null + value: 10 } }, { key_as_string: '2018-07-02T10:15:00.000Z', - key: 1530526500000, + key: 30000, doc_count: 2, anomaly_score: { value: 0 }, upper: { - value: 54158.77731018045 + value: null }, lower: { - value: 16034.081569306454 + value: null } } ] - }, - top_hits: { - hits: { - total: 2, - max_score: 0, - hits: [ - { - _index: '.ml-anomalies-shared', - _type: 'doc', - _id: - 'opbeans-node-request-high_mean_response_time_model_plot_1530522900000_900_0_29791_0', - _score: 0, - _source: { - bucket_span: 900 - } - } - ] - } } } }; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/mock-responses/mlBucketSpanResponse.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/mock-responses/mlBucketSpanResponse.ts new file mode 100644 index 000000000000000..a4e54d240f2047c --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/mock-responses/mlBucketSpanResponse.ts @@ -0,0 +1,32 @@ +/* + * 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. + */ + +export const mlBucketSpanResponse = { + took: 1, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0 + }, + hits: { + total: 192, + max_score: 1.0, + hits: [ + { + _index: '.ml-anomalies-shared', + _type: 'doc', + _id: + 'opbeans-go-request-high_mean_response_time_model_plot_1542636000000_900_0_29791_0', + _score: 1.0, + _source: { + bucket_span: 10 + } + } + ] + } +}; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/transform.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/transform.test.ts new file mode 100644 index 000000000000000..5f875b8fb377ef3 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/transform.test.ts @@ -0,0 +1,272 @@ +/* + * 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 { oc } from 'ts-optchain'; +import { ESBucket, ESResponse } from './fetcher'; +import { mlAnomalyResponse } from './mock-responses/mlAnomalyResponse'; +import { anomalySeriesTransform, replaceFirstAndLastBucket } from './transform'; + +describe('anomalySeriesTransform', () => { + it('should match snapshot', () => { + const getMlBucketSize = 10; + const bucketSize = 5; + const timeSeriesDates = [10000, 25000]; + const anomalySeries = anomalySeriesTransform( + mlAnomalyResponse, + getMlBucketSize, + bucketSize, + timeSeriesDates + ); + expect(anomalySeries).toMatchSnapshot(); + }); + + describe('anomalyScoreSeries', () => { + it('should only returns bucket within range and above threshold', () => { + const esResponse = getESResponse([ + { + key: 0, + anomaly_score: { value: 90 } + }, + { + key: 5000, + anomaly_score: { value: 0 } + }, + { + key: 10000, + anomaly_score: { value: 90 } + }, + { + key: 15000, + anomaly_score: { value: 0 } + } + ] as ESBucket[]); + + const getMlBucketSize = 10; + const bucketSize = 5; + const timeSeriesDates = [5000, 10000]; + const anomalySeries = anomalySeriesTransform( + esResponse, + getMlBucketSize, + bucketSize, + timeSeriesDates + ); + + const buckets = anomalySeries!.anomalyScoreSeries.data; + expect(buckets).toEqual([{ x: 20000, x0: 10000 }]); + }); + }); + + describe('anomalyBoundariesSeries', () => { + it('should trim buckets to time range', () => { + const esResponse = getESResponse([ + { + key: 0, + upper: { value: 15 }, + lower: { value: 10 } + }, + { + key: 5000, + upper: { value: 25 }, + lower: { value: 20 } + }, + { + key: 10000, + upper: { value: 35 }, + lower: { value: 30 } + }, + { + key: 15000, + upper: { value: 45 }, + lower: { value: 40 } + } + ] as ESBucket[]); + + const mlBucketSize = 10; + const bucketSize = 5; + const timeSeriesDates = [5000, 10000]; + const anomalySeries = anomalySeriesTransform( + esResponse, + mlBucketSize, + bucketSize, + timeSeriesDates + ); + + const buckets = anomalySeries!.anomalyBoundariesSeries.data; + expect(buckets).toEqual([ + { x: 5000, y: 25, y0: 20 }, + { x: 10000, y: 35, y0: 30 } + ]); + }); + + it('should replace first bucket in range', () => { + const esResponse = getESResponse([ + { + key: 0, + anomaly_score: { value: 0 }, + upper: { value: 15 }, + lower: { value: 10 } + }, + { + key: 5000, + anomaly_score: { value: 0 }, + upper: { value: null }, + lower: { value: null } + }, + { + key: 10000, + anomaly_score: { value: 0 }, + upper: { value: 25 }, + lower: { value: 20 } + } + ] as ESBucket[]); + + const getMlBucketSize = 10; + const bucketSize = 5; + const timeSeriesDates = [5000, 10000]; + const anomalySeries = anomalySeriesTransform( + esResponse, + getMlBucketSize, + bucketSize, + timeSeriesDates + ); + + const buckets = anomalySeries!.anomalyBoundariesSeries.data; + expect(buckets).toEqual([ + { x: 5000, y: 15, y0: 10 }, + { x: 10000, y: 25, y0: 20 } + ]); + }); + + it('should replace last bucket in range', () => { + const esResponse = getESResponse([ + { + key: 0, + anomaly_score: { value: 0 }, + upper: { value: 15 }, + lower: { value: 10 } + }, + { + key: 5000, + anomaly_score: { value: 0 }, + upper: { value: null }, + lower: { value: null } + }, + { + key: 10000, + anomaly_score: { value: 0 }, + upper: { value: null }, + lower: { value: null } + } + ] as ESBucket[]); + + const getMlBucketSize = 10; + const bucketSize = 5; + const timeSeriesDates = [5000, 10000]; + const anomalySeries = anomalySeriesTransform( + esResponse, + getMlBucketSize, + bucketSize, + timeSeriesDates + ); + + const buckets = anomalySeries!.anomalyBoundariesSeries.data; + expect(buckets).toEqual([ + { x: 5000, y: 15, y0: 10 }, + { x: 10000, y: 15, y0: 10 } + ]); + }); + }); +}); + +describe('replaceFirstAndLastBucket', () => { + it('should extend the first bucket', () => { + const buckets = [ + { + x: 0, + lower: 10, + upper: 20 + }, + { + x: 5, + lower: null, + upper: null + }, + { + x: 10, + lower: null, + upper: null + }, + { + x: 15, + lower: 30, + upper: 40 + } + ] as any; + + const timeSeriesDates = [10, 15]; + expect(replaceFirstAndLastBucket(buckets, timeSeriesDates)).toEqual([ + { x: 10, lower: 10, upper: 20 }, + { x: 15, lower: 30, upper: 40 } + ]); + }); + + it('should extend the last bucket', () => { + const buckets = [ + { + x: 10, + lower: 30, + upper: 40 + }, + { + x: 15, + lower: null, + upper: null + }, + { + x: 20, + lower: null, + upper: null + } + ] as any; + + const timeSeriesDates = [10, 15, 20]; + expect(replaceFirstAndLastBucket(buckets, timeSeriesDates)).toEqual([ + { x: 10, lower: 30, upper: 40 }, + { x: 15, lower: null, upper: null }, + { x: 20, lower: 30, upper: 40 } + ]); + }); +}); + +function getESResponse(buckets: ESBucket[]): ESResponse { + return { + took: 3, + timed_out: false, + _shards: { + total: 5, + successful: 5, + skipped: 0, + failed: 0 + }, + hits: { + total: 10, + max_score: 0, + hits: [] + }, + aggregations: { + ml_avg_response_times: { + buckets: buckets.map(bucket => { + return { + ...bucket, + lower: { value: oc(bucket).lower.value(null) }, + upper: { value: oc(bucket).upper.value(null) }, + anomaly_score: { value: oc(bucket).anomaly_score.value(null) } + }; + }) + } + } + }; +} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/transform.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/transform.ts new file mode 100644 index 000000000000000..241aa111df7b5b2 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_series/transform.ts @@ -0,0 +1,172 @@ +/* + * 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 { first, last } from 'lodash'; +import { rgba } from 'polished'; +import { oc } from 'ts-optchain'; +import { colors } from 'x-pack/plugins/apm/common/variables'; +import { ESResponse } from './fetcher'; + +interface IBucket { + x: number; + anomalyScore: number | null; + lower: number | null; + upper: number | null; +} + +// TODO: remove duplication between this and chartSelector +interface Coordinate { + x: number; + y?: number | null; +} + +// TODO: remove duplication between this and chartSelector +interface TimeSerie { + title: string; + titleShort?: string; + hideLegend?: boolean; + hideTooltipValue?: boolean; + data: Coordinate[]; + legendValue?: string; + type: string; + color: string; + areaColor?: string; +} + +export interface AnomalyTimeSeriesResponse { + anomalyScoreSeries: TimeSerie; + anomalyBoundariesSeries: TimeSerie; +} + +export function anomalySeriesTransform( + response: ESResponse | undefined, + mlBucketSize: number, + bucketSize: number, + timeSeriesDates: number[] +): AnomalyTimeSeriesResponse | undefined { + if (!response) { + return; + } + + const buckets = oc(response) + .aggregations.ml_avg_response_times.buckets([]) + .map(bucket => { + return { + x: bucket.key, + anomalyScore: bucket.anomaly_score.value, + lower: bucket.lower.value, + upper: bucket.upper.value + }; + }); + + const bucketSizeInMillis = Math.max(bucketSize, mlBucketSize) * 1000; + + return { + anomalyScoreSeries: { + title: 'Anomaly score', + hideLegend: true, + hideTooltipValue: true, + data: getAnomalyScoreDataPoints( + buckets, + timeSeriesDates, + bucketSizeInMillis + ), + type: 'areaMaxHeight', + color: 'none', + areaColor: rgba(colors.apmRed, 0.1) + // areaColor: 'rgba(60, 100, 50, 0.5)' + }, + anomalyBoundariesSeries: { + title: 'Anomaly Boundaries', + hideLegend: true, + hideTooltipValue: true, + data: getAnomalyBoundaryDataPoints(buckets, timeSeriesDates), + type: 'area', + color: 'none', + // areaColor: 'rgba(30, 100, 30, 0.5)' + areaColor: rgba(colors.apmBlue, 0.1) + } + }; +} + +export function getAnomalyScoreDataPoints( + buckets: IBucket[], + timeSeriesDates: number[], + bucketSizeInMillis: number +): Coordinate[] { + const ANOMALY_THRESHOLD = 75; + const firstDate = first(timeSeriesDates); + const lastDate = last(timeSeriesDates); + + return buckets + .filter( + bucket => + bucket.anomalyScore !== null && bucket.anomalyScore > ANOMALY_THRESHOLD + ) + .filter(isInDateRange(firstDate, lastDate)) + .map(bucket => { + return { + x0: bucket.x, + x: bucket.x + bucketSizeInMillis + }; + }); +} + +export function getAnomalyBoundaryDataPoints( + buckets: IBucket[], + timeSeriesDates: number[] +): Coordinate[] { + return replaceFirstAndLastBucket(buckets, timeSeriesDates) + .filter(bucket => bucket.lower !== null) + .map(bucket => { + return { + x: bucket.x, + y0: bucket.lower, + y: bucket.upper + }; + }); +} + +export function replaceFirstAndLastBucket( + buckets: IBucket[], + timeSeriesDates: number[] +) { + const firstDate = first(timeSeriesDates); + const lastDate = last(timeSeriesDates); + + const preBucketWithValue = buckets + .filter(p => p.x <= firstDate) + .reverse() + .find(p => p.lower !== null); + + const bucketsInRange = buckets.filter(isInDateRange(firstDate, lastDate)); + + // replace first bucket if it is null + const firstBucket = first(bucketsInRange); + if (preBucketWithValue && firstBucket && firstBucket.lower === null) { + firstBucket.lower = preBucketWithValue.lower; + firstBucket.upper = preBucketWithValue.upper; + } + + const lastBucketWithValue = [...buckets] + .reverse() + .find(p => p.lower !== null); + + // replace last bucket if it is null + const lastBucket = last(bucketsInRange); + if (lastBucketWithValue && lastBucket && lastBucket.lower === null) { + lastBucket.lower = lastBucketWithValue.lower; + lastBucket.upper = lastBucketWithValue.upper; + } + + return bucketsInRange; +} + +// anomaly time series contain one or more buckets extra in the beginning +// these extra buckets should be removed +function isInDateRange(firstDate: number, lastDate: number) { + return (p: IBucket) => p.x >= firstDate && p.x <= lastDate; +} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/__snapshots__/get_buckets_with_initial_anomaly_bounds.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/__snapshots__/get_buckets_with_initial_anomaly_bounds.test.ts.snap deleted file mode 100644 index 072a819e7539882..000000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/__snapshots__/get_buckets_with_initial_anomaly_bounds.test.ts.snap +++ /dev/null @@ -1,46 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`get_buckets_with_initial_anomaly_bounds should return correct buckets 1`] = ` -Array [ - Object { - "anomalyScore": 0, - "lower": 17688.182675688193, - "upper": 50381.01051622894, - }, - Object { - "anomalyScore": null, - "lower": null, - "upper": null, - }, - Object { - "anomalyScore": null, - "lower": null, - "upper": null, - }, - Object { - "anomalyScore": 0, - "lower": 16034.081569306454, - "upper": 54158.77731018045, - }, - Object { - "anomalyScore": null, - "lower": null, - "upper": null, - }, - Object { - "anomalyScore": 0, - "lower": 16034.081569306454, - "upper": 54158.77731018045, - }, - Object { - "anomalyScore": null, - "lower": null, - "upper": null, - }, - Object { - "anomalyScore": 0, - "lower": 16034.081569306454, - "upper": 54158.77731018045, - }, -] -`; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/__snapshots__/transform.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/__snapshots__/transform.test.ts.snap deleted file mode 100644 index 1eeac5ae02e5ef4..000000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/__snapshots__/transform.test.ts.snap +++ /dev/null @@ -1,49 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`anomalyAggsTransform should match snapshot 1`] = ` -Object { - "bucketSize": 900, - "buckets": Array [ - Object { - "anomalyScore": null, - "lower": null, - "upper": null, - }, - Object { - "anomalyScore": null, - "lower": null, - "upper": null, - }, - Object { - "anomalyScore": null, - "lower": null, - "upper": null, - }, - Object { - "anomalyScore": 0, - "lower": 16034.081569306454, - "upper": 54158.77731018045, - }, - Object { - "anomalyScore": null, - "lower": null, - "upper": null, - }, - Object { - "anomalyScore": 0, - "lower": 16034.081569306454, - "upper": 54158.77731018045, - }, - Object { - "anomalyScore": null, - "lower": null, - "upper": null, - }, - Object { - "anomalyScore": 0, - "lower": 16034.081569306454, - "upper": 54158.77731018045, - }, - ], -} -`; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/index.ts deleted file mode 100644 index c3df288527442d0..000000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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 { anomalyAggsFetcher, IOptions } from './fetcher'; -import { anomalyAggsTransform } from './transform'; - -export async function getAnomalyAggs(options: IOptions) { - const response = await anomalyAggsFetcher(options); - return anomalyAggsTransform(response); -} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/transform.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/transform.test.ts deleted file mode 100644 index 1b1dbf8848cfcea..000000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/transform.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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 { mainBucketsResponse } from '../mock-responses/mainBucketsResponse'; -import { anomalyAggsTransform } from './transform'; - -describe('anomalyAggsTransform', () => { - it('should return null if response is empty', () => { - expect(anomalyAggsTransform(null)).toBe(null); - }); - - it('should match snapshot', () => { - expect(anomalyAggsTransform(mainBucketsResponse)).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/transform.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/transform.ts deleted file mode 100644 index d6ebb3ba7a3a758..000000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_anomaly_aggs/transform.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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 { oc } from 'ts-optchain'; -import { ESResponse } from './fetcher'; - -export interface AvgAnomalyBucket { - anomalyScore: number | null; - lower: number | null; - upper: number | null; -} - -export function anomalyAggsTransform(response: ESResponse) { - if (!response) { - return null; - } - - const buckets = oc(response) - .aggregations.ml_avg_response_times.buckets([]) - .map(bucket => { - return { - anomalyScore: bucket.anomaly_score.value, - lower: bucket.lower.value, - upper: bucket.upper.value - }; - }); - - return { - buckets, - bucketSize: oc( - response - ).aggregations.top_hits.hits.hits[0]._source.bucket_span(0) - }; -} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_buckets_with_initial_anomaly_bounds.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_buckets_with_initial_anomaly_bounds.test.ts deleted file mode 100644 index f1fb18e2a035a31..000000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_buckets_with_initial_anomaly_bounds.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 { getAnomalyAggs } from './get_anomaly_aggs'; -import { AvgAnomalyBucket } from './get_anomaly_aggs/transform'; -import { getBucketWithInitialAnomalyBounds } from './get_buckets_with_initial_anomaly_bounds'; -import { firstBucketsResponse } from './mock-responses/firstBucketsResponse'; -import { mainBucketsResponse } from './mock-responses/mainBucketsResponse'; - -describe('get_buckets_with_initial_anomaly_bounds', () => { - let buckets: AvgAnomalyBucket[]; - let mainBuckets: AvgAnomalyBucket[]; - - beforeEach(async () => { - const response = await getAnomalyAggs({ - serviceName: 'myServiceName', - transactionType: 'myTransactionType', - intervalString: '', - client: () => mainBucketsResponse as any, - start: 0, - end: 1 - }); - - mainBuckets = response!.buckets; - buckets = await getBucketWithInitialAnomalyBounds({ - serviceName: 'myServiceName', - transactionType: 'myTransactionType', - start: 1530523322742, - client: () => firstBucketsResponse as any, - buckets: mainBuckets, - bucketSize: 900 - }); - }); - - it('should return correct buckets', () => { - expect(buckets).toMatchSnapshot(); - }); - - it('should not change the number of buckets', () => { - expect(mainBuckets.length).toEqual(buckets.length); - }); - - it('should replace the first bucket but leave all other buckets the same', () => { - buckets.forEach((bucket, i) => { - if (i === 0) { - expect(mainBuckets[0]).not.toEqual(bucket); - } else { - expect(mainBuckets[i]).toBe(bucket); - } - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_buckets_with_initial_anomaly_bounds.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_buckets_with_initial_anomaly_bounds.ts deleted file mode 100644 index a56b6566716728c..000000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/get_buckets_with_initial_anomaly_bounds.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 { last } from 'lodash'; -import { ESClient } from '../../../helpers/setup_request'; -import { getAnomalyAggs } from './get_anomaly_aggs'; -import { AvgAnomalyBucket } from './get_anomaly_aggs/transform'; - -interface Props { - serviceName: string; - transactionType: string; - buckets: AvgAnomalyBucket[]; - bucketSize: number; - start: number; - client: ESClient; -} - -export async function getBucketWithInitialAnomalyBounds({ - serviceName, - transactionType, - buckets, - bucketSize, - start, - client -}: Props) { - // abort if first bucket already has values for initial anomaly bounds - if (buckets[0].lower || !bucketSize) { - return buckets; - } - - const newStart = start - bucketSize * 1000; - const newEnd = start; - - const aggs = await getAnomalyAggs({ - serviceName, - transactionType, - intervalString: `${bucketSize}s`, - client, - start: newStart, - end: newEnd - }); - - if (!aggs) { - return buckets; - } - - const firstBucketWithBounds = last( - aggs.buckets.filter(bucket => bucket.lower) - ); - - if (!firstBucketWithBounds) { - return buckets; - } - - return replaceFirstItem(buckets, firstBucketWithBounds); -} - -// copy array and replace first item -function replaceFirstItem(array: T[], value: T) { - const ret = array.slice(0); - ret[0] = value; - return ret; -} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/index.test.ts deleted file mode 100644 index 468c2c7e3a07488..000000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/index.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 { getAvgResponseTimeAnomalies } from '.'; -import { firstBucketsResponse } from './mock-responses/firstBucketsResponse'; -import { mainBucketsResponse } from './mock-responses/mainBucketsResponse'; - -describe('get_avg_response_time_anomalies', () => { - it('', async () => { - const clientSpy = jest - .fn() - .mockResolvedValueOnce(mainBucketsResponse) - .mockResolvedValueOnce(firstBucketsResponse); - - const avgAnomalies = await getAvgResponseTimeAnomalies({ - serviceName: 'myServiceName', - transactionType: 'myTransactionType', - setup: { - start: 1528113600000, - end: 1528977600000, - client: clientSpy, - config: { - get: () => 'myIndex' as any - } - } - }); - - expect(avgAnomalies).toEqual({ - bucketSizeAsMillis: 10800000, - buckets: [ - { - anomalyScore: 0, - lower: 17688.182675688193, - upper: 50381.01051622894 - }, - { anomalyScore: null, lower: null, upper: null }, - { - anomalyScore: 0, - lower: 16034.081569306454, - upper: 54158.77731018045 - }, - { anomalyScore: null, lower: null, upper: null }, - { - anomalyScore: 0, - lower: 16034.081569306454, - upper: 54158.77731018045 - }, - { anomalyScore: null, lower: null, upper: null } - ] - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/index.ts deleted file mode 100644 index 31ee4a9d1a6d7f3..000000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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 { getBucketSize } from '../../../helpers/get_bucket_size'; -import { IOptions } from '../get_timeseries_data'; -import { getAnomalyAggs } from './get_anomaly_aggs'; -import { AvgAnomalyBucket } from './get_anomaly_aggs/transform'; -import { getBucketWithInitialAnomalyBounds } from './get_buckets_with_initial_anomaly_bounds'; - -export interface IAvgAnomalies { - bucketSizeAsMillis: number; - buckets: AvgAnomalyBucket[]; -} - -export type IAvgAnomaliesResponse = IAvgAnomalies | undefined; - -export async function getAvgResponseTimeAnomalies({ - serviceName, - transactionType, - transactionName, - setup -}: IOptions): Promise { - const { start, end, client } = setup; - const { intervalString, bucketSize } = getBucketSize(start, end, 'auto'); - - // don't fetch anomalies for transaction details page - if (transactionName) { - return; - } - - const aggs = await getAnomalyAggs({ - serviceName, - transactionType, - intervalString, - client, - start, - end - }); - - if (!aggs) { - return; - } - - const buckets = await getBucketWithInitialAnomalyBounds({ - serviceName, - transactionType, - buckets: aggs.buckets.slice(1, -1), - bucketSize: aggs.bucketSize, - start, - client - }); - - return { - buckets, - bucketSizeAsMillis: Math.max(bucketSize, aggs.bucketSize) * 1000 - }; -} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/mock-responses/firstBucketsResponse.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/mock-responses/firstBucketsResponse.ts deleted file mode 100644 index d23807764af85a6..000000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_avg_response_time_anomalies/mock-responses/firstBucketsResponse.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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 { ESResponse } from '../get_anomaly_aggs/fetcher'; - -export const firstBucketsResponse: ESResponse = { - took: 22, - timed_out: false, - _shards: { - total: 5, - successful: 5, - skipped: 0, - failed: 0 - }, - hits: { - total: 2, - max_score: 0, - hits: [] - }, - aggregations: { - ml_avg_response_times: { - buckets: [ - { - key_as_string: '2018-07-02T09:00:00.000Z', - key: 1530522000000, - doc_count: 0, - anomaly_score: { - value: null - }, - upper: { - value: null - }, - lower: { - value: null - } - }, - { - key_as_string: '2018-07-02T09:08:20.000Z', - key: 1530522500000, - doc_count: 2, - anomaly_score: { - value: 0 - }, - upper: { - value: 50381.01051622894 - }, - lower: { - value: 17688.182675688193 - } - }, - { - key_as_string: '2018-07-02T09:16:40.000Z', - key: 1530523000000, - doc_count: 0, - anomaly_score: { - value: null - }, - upper: { - value: null - }, - lower: { - value: null - } - } - ] - }, - top_hits: { - hits: { - total: 2, - max_score: 0, - hits: [ - { - _index: '.ml-anomalies-shared', - _type: 'doc', - _id: - 'opbeans-node-request-high_mean_response_time_model_plot_1530522900000_900_0_29791_0', - _score: 0, - _source: { - bucket_span: 900 - } - } - ] - } - } - } -}; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/transform.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/transform.test.ts.snap index d94348c4f85131f..85299162d1c909f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/transform.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/transform.test.ts.snap @@ -166,7 +166,6 @@ Object { 48256.049354513096, 52360.30017052116, ], - "avgAnomalies": undefined, "p95": Array [ 80738.78571428556, 77058.03529411761, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index 7360114c1df8d27..9457a2ffc0b4a83 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -5,7 +5,6 @@ */ import { AggregationSearchResponse } from 'elasticsearch'; -import { IOptions } from '.'; import { SERVICE_NAME, TRANSACTION_DURATION, @@ -14,6 +13,7 @@ import { TRANSACTION_TYPE } from '../../../../../common/constants'; import { getBucketSize } from '../../../helpers/get_bucket_size'; +import { Setup } from '../../../helpers/setup_request'; interface ResponseTimeBucket { key_as_string: string; @@ -63,7 +63,12 @@ export function timeseriesFetcher({ transactionType, transactionName, setup -}: IOptions): Promise { +}: { + serviceName: string; + transactionType: string; + transactionName?: string; + setup: Setup; +}): Promise { const { start, end, esFilterQuery, client, config } = setup; const { intervalString } = getBucketSize(start, end, 'auto'); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts index 8327193d82111ab..9cd6ce11968ced9 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts @@ -6,26 +6,37 @@ import { getBucketSize } from '../../../helpers/get_bucket_size'; import { Setup } from '../../../helpers/setup_request'; -import { getAvgResponseTimeAnomalies } from '../get_avg_response_time_anomalies'; +import { getAnomalySeries } from '../get_anomaly_series'; +import { AnomalyTimeSeriesResponse } from '../get_anomaly_series/transform'; import { timeseriesFetcher } from './fetcher'; -import { timeseriesTransformer } from './transform'; +import { ApmTimeSeriesResponse, timeseriesTransformer } from './transform'; -export interface IOptions { +export interface TimeSeriesAPIResponse extends ApmTimeSeriesResponse { + anomalyTimeSeries?: AnomalyTimeSeriesResponse; +} + +export async function getTimeseriesData(options: { serviceName: string; transactionType: string; transactionName?: string; setup: Setup; -} - -export async function getTimeseriesData(options: IOptions) { +}): Promise { const { start, end } = options.setup; const { bucketSize } = getBucketSize(start, end, 'auto'); - const avgAnomaliesResponse = await getAvgResponseTimeAnomalies(options); const timeseriesResponse = await timeseriesFetcher(options); - return timeseriesTransformer({ + const transformedTimeSeries = timeseriesTransformer({ timeseriesResponse, - avgAnomaliesResponse, bucketSize }); + + const anomalyTimeSeries = await getAnomalySeries({ + ...options, + timeSeriesDates: transformedTimeSeries.dates + }); + + return { + ...transformedTimeSeries, + anomalyTimeSeries + }; } diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts index ecf64e393b191ac..e83218d1faf636f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts @@ -5,19 +5,15 @@ */ import { first, last } from 'lodash'; +import { TimeSeriesAPIResponse } from '.'; import { timeseriesResponse } from './mock-responses/timeseries_response'; -import { - getTpmBuckets, - TimeSeriesAPIResponse, - timeseriesTransformer -} from './transform'; +import { getTpmBuckets, timeseriesTransformer } from './transform'; describe('timeseriesTransformer', () => { let res: TimeSeriesAPIResponse; beforeEach(async () => { res = await timeseriesTransformer({ timeseriesResponse, - avgAnomaliesResponse: undefined, bucketSize: 12 }); }); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts index ff9b28239b521d7..39fa50e0349d329 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts @@ -7,19 +7,17 @@ import { isNumber, round, sortBy } from 'lodash'; import mean from 'lodash.mean'; import { oc } from 'ts-optchain'; -import { IAvgAnomaliesResponse } from '../get_avg_response_time_anomalies'; import { ESResponse } from './fetcher'; type MaybeNumber = number | null; -export interface TimeSeriesAPIResponse { +export interface ApmTimeSeriesResponse { totalHits: number; dates: number[]; responseTimes: { avg: MaybeNumber[]; p95: MaybeNumber[]; p99: MaybeNumber[]; - avgAnomalies?: IAvgAnomaliesResponse; }; tpmBuckets: Array<{ key: string; @@ -31,13 +29,11 @@ export interface TimeSeriesAPIResponse { export function timeseriesTransformer({ timeseriesResponse, - avgAnomaliesResponse, bucketSize }: { timeseriesResponse: ESResponse; - avgAnomaliesResponse: IAvgAnomaliesResponse; bucketSize: number; -}): TimeSeriesAPIResponse { +}): ApmTimeSeriesResponse { const aggs = timeseriesResponse.aggregations; const overallAvgDuration = oc(aggs).overall_avg_duration.value(); @@ -56,8 +52,7 @@ export function timeseriesTransformer({ responseTimes: { avg, p95, - p99, - avgAnomalies: avgAnomaliesResponse + p99 }, tpmBuckets, overallAvgDuration