From f4a83a0852507a7a6a72af3a31cfced76124f14c Mon Sep 17 00:00:00 2001
From: Walter Rafelsberger
Date: Fri, 27 Aug 2021 13:08:50 +0200
Subject: [PATCH] [ML] APM Latency Correlations: Fix empty state (#109813)
- Correctly renders the empty chart state when no data is available.
- Hides the "Click drag to select" and trace samples message when the chart shows an empty state to avoid redundant info.
- Adds jest unit tests that would fail with the previously visible loading indicators.
- Fix a bug with cancelling search strategies.
---
.../search_strategies/correlations/types.ts | 9 ++
.../app/correlations/empty_state_prompt.tsx | 3 +-
.../failed_transactions_correlations.tsx | 49 +++---
.../latency_correlations.test.tsx | 131 +++++++++++++++
.../app/correlations/latency_correlations.tsx | 30 ++--
.../distribution/index.test.ts | 22 ---
.../distribution/index.test.tsx | 151 ++++++++++++++++++
.../distribution/index.tsx | 58 ++++---
.../transaction_details/trace_samples_tab.tsx | 28 ++--
.../shared/charts/chart_container.tsx | 4 +-
.../transaction_distribution_chart/index.tsx | 45 ++++--
...ailed_transactions_correlations_fetcher.ts | 93 +++++------
.../use_transaction_distribution_fetcher.ts | 136 ++++++++--------
...ransaction_latency_correlations_fetcher.ts | 143 +++++++++--------
.../correlations/search_strategy.ts | 19 ++-
15 files changed, 621 insertions(+), 300 deletions(-)
create mode 100644 x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx
delete mode 100644 x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.ts
create mode 100644 x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx
diff --git a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts
index 703106628f561..886c5fd6161d8 100644
--- a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts
+++ b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts
@@ -52,3 +52,12 @@ export interface AsyncSearchProviderProgress {
loadedFieldValuePairs: number;
loadedHistograms: number;
}
+
+export interface SearchServiceRawResponse {
+ ccsWarning: boolean;
+ log: string[];
+ overallHistogram?: HistogramItem[];
+ percentileThresholdValue?: number;
+ took: number;
+ values: SearchServiceValue[];
+}
diff --git a/x-pack/plugins/apm/public/components/app/correlations/empty_state_prompt.tsx b/x-pack/plugins/apm/public/components/app/correlations/empty_state_prompt.tsx
index 57e57a526baff..9b161fc1b9fa9 100644
--- a/x-pack/plugins/apm/public/components/app/correlations/empty_state_prompt.tsx
+++ b/x-pack/plugins/apm/public/components/app/correlations/empty_state_prompt.tsx
@@ -33,8 +33,7 @@ export function CorrelationsEmptyStatePrompt() {
id="xpack.apm.correlations.noCorrelationsTextLine1"
defaultMessage="Correlations will only be identified if they have significant impact."
/>
-
-
+
(enableInspectEsQueries);
- const searchServicePrams: SearchServiceParams = {
- environment,
- kuery,
- serviceName,
- transactionName,
- transactionType,
- start,
- end,
- };
-
const result = useFailedTransactionsCorrelationsFetcher();
const {
@@ -93,26 +82,30 @@ export function FailedTransactionsCorrelations({
} = result;
const startFetchHandler = useCallback(() => {
- startFetch(searchServicePrams);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [environment, serviceName, kuery, start, end]);
+ startFetch({
+ environment,
+ kuery,
+ serviceName,
+ transactionName,
+ transactionType,
+ start,
+ end,
+ });
+ }, [
+ startFetch,
+ environment,
+ serviceName,
+ transactionName,
+ transactionType,
+ kuery,
+ start,
+ end,
+ ]);
- // start fetching on load
- // we want this effect to execute exactly once after the component mounts
useEffect(() => {
- if (isRunning) {
- cancelFetch();
- }
-
startFetchHandler();
-
- return () => {
- // cancel any running async partial request when unmounting the component
- // we want this effect to execute exactly once after the component mounts
- cancelFetch();
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [startFetchHandler]);
+ return cancelFetch;
+ }, [cancelFetch, startFetchHandler]);
const [
selectedSignificantTerm,
diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx
new file mode 100644
index 0000000000000..b0da5b6d60d74
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx
@@ -0,0 +1,131 @@
+/*
+ * 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 { render, screen, waitFor } from '@testing-library/react';
+import { createMemoryHistory } from 'history';
+import React, { ReactNode } from 'react';
+import { of } from 'rxjs';
+
+import { __IntlProvider as IntlProvider } from '@kbn/i18n/react';
+
+import { CoreStart } from 'kibana/public';
+import { merge } from 'lodash';
+import { dataPluginMock } from 'src/plugins/data/public/mocks';
+import type { IKibanaSearchResponse } from 'src/plugins/data/public';
+import { EuiThemeProvider } from 'src/plugins/kibana_react/common';
+import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
+import type { SearchServiceRawResponse } from '../../../../common/search_strategies/correlations/types';
+import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider';
+import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context';
+import {
+ mockApmPluginContextValue,
+ MockApmPluginContextWrapper,
+} from '../../../context/apm_plugin/mock_apm_plugin_context';
+import { fromQuery } from '../../shared/Links/url_helpers';
+
+import { LatencyCorrelations } from './latency_correlations';
+
+function Wrapper({
+ children,
+ dataSearchResponse,
+}: {
+ children?: ReactNode;
+ dataSearchResponse: IKibanaSearchResponse;
+}) {
+ const mockDataSearch = jest.fn(() => of(dataSearchResponse));
+
+ const dataPluginMockStart = dataPluginMock.createStartContract();
+ const KibanaReactContext = createKibanaReactContext({
+ data: {
+ ...dataPluginMockStart,
+ search: {
+ ...dataPluginMockStart.search,
+ search: mockDataSearch,
+ },
+ },
+ usageCollection: { reportUiCounter: () => {} },
+ } as Partial);
+
+ const httpGet = jest.fn();
+
+ const history = createMemoryHistory();
+ jest.spyOn(history, 'push');
+ jest.spyOn(history, 'replace');
+
+ history.replace({
+ pathname: '/services/the-service-name/transactions/view',
+ search: fromQuery({ transactionName: 'the-transaction-name' }),
+ });
+
+ const mockPluginContext = (merge({}, mockApmPluginContextValue, {
+ core: { http: { get: httpGet } },
+ }) as unknown) as ApmPluginContextValue;
+
+ return (
+
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
+
+describe('correlations', () => {
+ describe('LatencyCorrelations', () => {
+ it('shows loading indicator when the service is running and returned no results yet', async () => {
+ render(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('apmCorrelationsChart')).toBeInTheDocument();
+ expect(screen.getByTestId('loading')).toBeInTheDocument();
+ });
+ });
+
+ it("doesn't show loading indicator when the service isn't running", async () => {
+ render(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('apmCorrelationsChart')).toBeInTheDocument();
+ expect(screen.queryByTestId('loading')).toBeNull(); // it doesn't exist
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx
index 74702e621a0ba..ad8a56a3ac6f9 100644
--- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx
+++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx
@@ -61,7 +61,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) {
const {
query: { kuery, environment, rangeFrom, rangeTo },
- } = useApmParams('/services/:serviceName');
+ } = useApmParams('/services/:serviceName/transactions/view');
const { urlParams } = useUrlParams();
@@ -95,25 +95,21 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) {
end,
percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD,
});
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [environment, serviceName, kuery, start, end]);
+ }, [
+ startFetch,
+ environment,
+ serviceName,
+ transactionName,
+ transactionType,
+ kuery,
+ start,
+ end,
+ ]);
- // start fetching on load
- // we want this effect to execute exactly once after the component mounts
useEffect(() => {
- if (isRunning) {
- cancelFetch();
- }
-
startFetchHandler();
-
- return () => {
- // cancel any running async partial request when unmounting the component
- // we want this effect to execute exactly once after the component mounts
- cancelFetch();
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [startFetchHandler]);
+ return cancelFetch;
+ }, [cancelFetch, startFetchHandler]);
useEffect(() => {
if (isErrorMessage(error)) {
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.ts
deleted file mode 100644
index f541c16e655ab..0000000000000
--- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.ts
+++ /dev/null
@@ -1,22 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { getFormattedSelection } from './index';
-
-describe('transaction_details/distribution', () => {
- describe('getFormattedSelection', () => {
- it('displays only one unit if from and to share the same unit', () => {
- expect(getFormattedSelection([10000, 100000])).toEqual('10 - 100 ms');
- });
-
- it('displays two units when from and to have different units', () => {
- expect(getFormattedSelection([100000, 1000000000])).toEqual(
- '100 ms - 17 min'
- );
- });
- });
-});
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx
new file mode 100644
index 0000000000000..5a9977b373c33
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx
@@ -0,0 +1,151 @@
+/*
+ * 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 { render, screen, waitFor } from '@testing-library/react';
+import { createMemoryHistory } from 'history';
+import React, { ReactNode } from 'react';
+import { of } from 'rxjs';
+
+import { CoreStart } from 'kibana/public';
+import { merge } from 'lodash';
+import { dataPluginMock } from 'src/plugins/data/public/mocks';
+import type { IKibanaSearchResponse } from 'src/plugins/data/public';
+import { EuiThemeProvider } from 'src/plugins/kibana_react/common';
+import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
+import type { SearchServiceRawResponse } from '../../../../../common/search_strategies/correlations/types';
+import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider';
+import { ApmPluginContextValue } from '../../../../context/apm_plugin/apm_plugin_context';
+import {
+ mockApmPluginContextValue,
+ MockApmPluginContextWrapper,
+} from '../../../../context/apm_plugin/mock_apm_plugin_context';
+import { fromQuery } from '../../../shared/Links/url_helpers';
+
+import { getFormattedSelection, TransactionDistribution } from './index';
+
+function Wrapper({
+ children,
+ dataSearchResponse,
+}: {
+ children?: ReactNode;
+ dataSearchResponse: IKibanaSearchResponse;
+}) {
+ const mockDataSearch = jest.fn(() => of(dataSearchResponse));
+
+ const dataPluginMockStart = dataPluginMock.createStartContract();
+ const KibanaReactContext = createKibanaReactContext({
+ data: {
+ ...dataPluginMockStart,
+ search: {
+ ...dataPluginMockStart.search,
+ search: mockDataSearch,
+ },
+ },
+ usageCollection: { reportUiCounter: () => {} },
+ } as Partial);
+
+ const httpGet = jest.fn();
+
+ const history = createMemoryHistory();
+ jest.spyOn(history, 'push');
+ jest.spyOn(history, 'replace');
+
+ history.replace({
+ pathname: '/services/the-service-name/transactions/view',
+ search: fromQuery({ transactionName: 'the-transaction-name' }),
+ });
+
+ const mockPluginContext = (merge({}, mockApmPluginContextValue, {
+ core: { http: { get: httpGet } },
+ }) as unknown) as ApmPluginContextValue;
+
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
+
+describe('transaction_details/distribution', () => {
+ describe('getFormattedSelection', () => {
+ it('displays only one unit if from and to share the same unit', () => {
+ expect(getFormattedSelection([10000, 100000])).toEqual('10 - 100 ms');
+ });
+
+ it('displays two units when from and to have different units', () => {
+ expect(getFormattedSelection([100000, 1000000000])).toEqual(
+ '100 ms - 17 min'
+ );
+ });
+ });
+
+ describe('TransactionDistribution', () => {
+ it('shows loading indicator when the service is running and returned no results yet', async () => {
+ const onHasData = jest.fn();
+ render(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('apmCorrelationsChart')).toBeInTheDocument();
+ expect(screen.getByTestId('loading')).toBeInTheDocument();
+ expect(onHasData).toHaveBeenLastCalledWith(false);
+ });
+ });
+
+ it("doesn't show loading indicator when the service isn't running", async () => {
+ const onHasData = jest.fn();
+ render(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('apmCorrelationsChart')).toBeInTheDocument();
+ expect(screen.queryByTestId('loading')).toBeNull(); // it doesn't exist
+ expect(onHasData).toHaveBeenLastCalledWith(false);
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx
index 86ebc04944cd5..2da61bc0fc555 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useEffect } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import { BrushEndListener, XYBrushArea } from '@elastic/charts';
import {
EuiBadge,
@@ -21,7 +21,10 @@ import { getDurationFormatter } from '../../../../../common/utils/formatters';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { useTransactionDistributionFetcher } from '../../../../hooks/use_transaction_distribution_fetcher';
-import { TransactionDistributionChart } from '../../../shared/charts/transaction_distribution_chart';
+import {
+ OnHasData,
+ TransactionDistributionChart,
+} from '../../../shared/charts/transaction_distribution_chart';
import { useUiTracker } from '../../../../../../observability/public';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useApmParams } from '../../../../hooks/use_apm_params';
@@ -47,10 +50,11 @@ export function getFormattedSelection(selection: Selection): string {
}`;
}
-interface Props {
+interface TransactionDistributionProps {
markerCurrentTransaction?: number;
onChartSelection: BrushEndListener;
onClearSelection: () => void;
+ onHasData: OnHasData;
selection?: Selection;
}
@@ -58,8 +62,9 @@ export function TransactionDistribution({
markerCurrentTransaction,
onChartSelection,
onClearSelection,
+ onHasData,
selection,
-}: Props) {
+}: TransactionDistributionProps) {
const {
core: { notifications },
} = useApmPluginContext();
@@ -68,7 +73,7 @@ export function TransactionDistribution({
const {
query: { kuery, environment, rangeFrom, rangeTo },
- } = useApmParams('/services/:serviceName');
+ } = useApmParams('/services/:serviceName/transactions/view');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
@@ -76,6 +81,16 @@ export function TransactionDistribution({
const { transactionName } = urlParams;
+ const [showSelection, setShowSelection] = useState(false);
+
+ const onTransactionDistributionHasData: OnHasData = useCallback(
+ (hasData) => {
+ setShowSelection(hasData);
+ onHasData(hasData);
+ },
+ [onHasData]
+ );
+
const emptySelectionText = i18n.translate(
'xpack.apm.transactionDetails.emptySelectionText',
{
@@ -93,17 +108,12 @@ export function TransactionDistribution({
const {
error,
percentileThresholdValue,
- isRunning,
startFetch,
cancelFetch,
transactionDistribution,
} = useTransactionDistributionFetcher();
- useEffect(() => {
- if (isRunning) {
- cancelFetch();
- }
-
+ const startFetchHandler = useCallback(() => {
startFetch({
environment,
kuery,
@@ -114,14 +124,21 @@ export function TransactionDistribution({
end,
percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD,
});
+ }, [
+ startFetch,
+ environment,
+ serviceName,
+ transactionName,
+ transactionType,
+ kuery,
+ start,
+ end,
+ ]);
- return () => {
- // cancel any running async partial request when unmounting the component
- // we want this effect to execute exactly once after the component mounts
- cancelFetch();
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [environment, serviceName, kuery, start, end]);
+ useEffect(() => {
+ startFetchHandler();
+ return cancelFetch;
+ }, [cancelFetch, startFetchHandler]);
useEffect(() => {
if (isErrorMessage(error)) {
@@ -166,7 +183,7 @@ export function TransactionDistribution({
- {!selection && (
+ {showSelection && !selection && (
)}
- {selection && (
+ {showSelection && selection && (
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx
index 0421fcd055d6c..ea02cfea5a941 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React from 'react';
+import React, { useState } from 'react';
import { EuiSpacer } from '@elastic/eui';
@@ -34,11 +34,17 @@ function TraceSamplesTab({
status: waterfallStatus,
} = useWaterfallFetcher();
+ const [
+ transactionDistributionHasData,
+ setTransactionDistributionHasData,
+ ] = useState(false);
+
return (
<>
-
+ {transactionDistributionHasData && (
+ <>
+
-
+
+ >
+ )}
>
);
}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx
index 4098fc5e696db..695e62b3b7d78 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx
@@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
-interface Props {
+export interface ChartContainerProps {
hasData: boolean;
status: FETCH_STATUS;
height: number;
@@ -24,7 +24,7 @@ export function ChartContainer({
status,
hasData,
id,
-}: Props) {
+}: ChartContainerProps) {
if (!hasData && status === FETCH_STATUS.LOADING) {
return ;
}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx
index c511a708058d3..a58a2887b1576 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useMemo } from 'react';
+import React, { useEffect, useMemo } from 'react';
import {
AnnotationDomainType,
AreaSeries,
@@ -35,7 +35,14 @@ import { HistogramItem } from '../../../../../common/search_strategies/correlati
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
import { useTheme } from '../../../../hooks/use_theme';
-import { ChartContainer } from '../chart_container';
+import { ChartContainer, ChartContainerProps } from '../chart_container';
+
+export type TransactionDistributionChartLoadingState = Pick<
+ ChartContainerProps,
+ 'hasData' | 'status'
+>;
+
+export type OnHasData = (hasData: boolean) => void;
interface TransactionDistributionChartProps {
field?: string;
@@ -46,6 +53,7 @@ interface TransactionDistributionChartProps {
markerPercentile: number;
overallHistogram?: HistogramItem[];
onChartSelection?: BrushEndListener;
+ onHasData?: OnHasData;
selection?: [number, number];
}
@@ -103,6 +111,7 @@ export function TransactionDistributionChart({
markerPercentile,
overallHistogram,
onChartSelection,
+ onHasData,
selection,
}: TransactionDistributionChartProps) {
const chartTheme = useChartTheme();
@@ -154,6 +163,24 @@ export function TransactionDistributionChart({
]
: undefined;
+ const chartLoadingState: TransactionDistributionChartLoadingState = useMemo(
+ () => ({
+ hasData:
+ Array.isArray(patchedOverallHistogram) &&
+ patchedOverallHistogram.length > 0,
+ status: Array.isArray(patchedOverallHistogram)
+ ? FETCH_STATUS.SUCCESS
+ : FETCH_STATUS.LOADING,
+ }),
+ [patchedOverallHistogram]
+ );
+
+ useEffect(() => {
+ if (onHasData) {
+ onHasData(chartLoadingState.hasData);
+ }
+ }, [chartLoadingState, onHasData]);
+
return (
0
- }
- status={
- Array.isArray(patchedOverallHistogram)
- ? FETCH_STATUS.SUCCESS
- : FETCH_STATUS.LOADING
- }
+ hasData={chartLoadingState.hasData}
+ status={chartLoadingState.status}
>
{
}));
}
- const startFetch = (params: SearchServiceParams) => {
- setFetchState((prevState) => ({
- ...prevState,
- error: undefined,
- isComplete: false,
- }));
- searchSubscription$.current?.unsubscribe();
- abortCtrl.current.abort();
- abortCtrl.current = new AbortController();
+ const startFetch = useCallback(
+ (params: SearchServiceParams) => {
+ setFetchState((prevState) => ({
+ ...prevState,
+ error: undefined,
+ isComplete: false,
+ }));
+ searchSubscription$.current?.unsubscribe();
+ abortCtrl.current.abort();
+ abortCtrl.current = new AbortController();
- const req = { params };
+ const req = { params };
- // Submit the search request using the `data.search` service.
- searchSubscription$.current = data.search
- .search>(req, {
- strategy: FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY,
- abortSignal: abortCtrl.current.signal,
- })
- .subscribe({
- next: (res: IKibanaSearchResponse) => {
- setResponse(res);
- if (isCompleteResponse(res)) {
- searchSubscription$.current?.unsubscribe();
+ // Submit the search request using the `data.search` service.
+ searchSubscription$.current = data.search
+ .search>(req, {
+ strategy: FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY,
+ abortSignal: abortCtrl.current.signal,
+ })
+ .subscribe({
+ next: (res: IKibanaSearchResponse) => {
+ setResponse(res);
+ if (isCompleteResponse(res)) {
+ searchSubscription$.current?.unsubscribe();
+ setFetchState((prevState) => ({
+ ...prevState,
+ isRunnning: false,
+ isComplete: true,
+ }));
+ } else if (isErrorResponse(res)) {
+ searchSubscription$.current?.unsubscribe();
+ setFetchState((prevState) => ({
+ ...prevState,
+ error: (res as unknown) as Error,
+ isRunning: false,
+ }));
+ }
+ },
+ error: (error: Error) => {
setFetchState((prevState) => ({
...prevState,
- isRunnning: false,
- isComplete: true,
+ error,
+ isRunning: false,
}));
- } else if (isErrorResponse(res)) {
- searchSubscription$.current?.unsubscribe();
- setFetchState((prevState) => ({
- ...prevState,
- error: (res as unknown) as Error,
- setIsRunning: false,
- }));
- }
- },
- error: (error: Error) => {
- setFetchState((prevState) => ({
- ...prevState,
- error,
- setIsRunning: false,
- }));
- },
- });
- };
+ },
+ });
+ },
+ [data.search, setFetchState]
+ );
- const cancelFetch = () => {
+ const cancelFetch = useCallback(() => {
searchSubscription$.current?.unsubscribe();
searchSubscription$.current = undefined;
abortCtrl.current.abort();
setFetchState((prevState) => ({
...prevState,
- setIsRunning: false,
+ isRunning: false,
}));
- };
+ }, [setFetchState]);
return {
...fetchState,
diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts
index 870dc8030d70b..2ff1b83ef1782 100644
--- a/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts
+++ b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { useRef, useState } from 'react';
+import { useCallback, useRef, useState } from 'react';
import type { Subscription } from 'rxjs';
import {
IKibanaSearchRequest,
@@ -14,31 +14,21 @@ import {
isErrorResponse,
} from '../../../../../src/plugins/data/public';
import type {
- HistogramItem,
SearchServiceParams,
- SearchServiceValue,
+ SearchServiceRawResponse,
} from '../../common/search_strategies/correlations/types';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import { ApmPluginStartDeps } from '../plugin';
-interface RawResponse {
- percentileThresholdValue?: number;
- took: number;
- values: SearchServiceValue[];
- overallHistogram: HistogramItem[];
- log: string[];
- ccsWarning: boolean;
-}
-
interface TransactionDistributionFetcherState {
error?: Error;
isComplete: boolean;
isRunning: boolean;
loaded: number;
- ccsWarning: RawResponse['ccsWarning'];
- log: RawResponse['log'];
- transactionDistribution?: RawResponse['overallHistogram'];
- percentileThresholdValue?: RawResponse['percentileThresholdValue'];
+ ccsWarning: SearchServiceRawResponse['ccsWarning'];
+ log: SearchServiceRawResponse['log'];
+ transactionDistribution?: SearchServiceRawResponse['overallHistogram'];
+ percentileThresholdValue?: SearchServiceRawResponse['percentileThresholdValue'];
timeTook?: number;
total: number;
}
@@ -63,7 +53,9 @@ export function useTransactionDistributionFetcher() {
const abortCtrl = useRef(new AbortController());
const searchSubscription$ = useRef();
- function setResponse(response: IKibanaSearchResponse) {
+ function setResponse(
+ response: IKibanaSearchResponse
+ ) {
setFetchState((prevState) => ({
...prevState,
isRunning: response.isRunning || false,
@@ -83,71 +75,81 @@ export function useTransactionDistributionFetcher() {
response.rawResponse?.percentileThresholdValue,
}
: {}),
+ // if loading is done but didn't return any data for the overall histogram,
+ // set it to an empty array so the consuming chart component knows loading is done.
+ ...(!response.isRunning &&
+ response.rawResponse?.overallHistogram === undefined
+ ? { transactionDistribution: [] }
+ : {}),
}));
}
- const startFetch = (
- params: Omit
- ) => {
- setFetchState((prevState) => ({
- ...prevState,
- error: undefined,
- isComplete: false,
- }));
- searchSubscription$.current?.unsubscribe();
- abortCtrl.current.abort();
- abortCtrl.current = new AbortController();
+ const startFetch = useCallback(
+ (params: Omit) => {
+ setFetchState((prevState) => ({
+ ...prevState,
+ error: undefined,
+ isComplete: false,
+ }));
+ searchSubscription$.current?.unsubscribe();
+ abortCtrl.current.abort();
+ abortCtrl.current = new AbortController();
- const searchServiceParams: SearchServiceParams = {
- ...params,
- analyzeCorrelations: false,
- };
- const req = { params: searchServiceParams };
+ const searchServiceParams: SearchServiceParams = {
+ ...params,
+ analyzeCorrelations: false,
+ };
+ const req = { params: searchServiceParams };
- // Submit the search request using the `data.search` service.
- searchSubscription$.current = data.search
- .search>(req, {
- strategy: 'apmCorrelationsSearchStrategy',
- abortSignal: abortCtrl.current.signal,
- })
- .subscribe({
- next: (res: IKibanaSearchResponse) => {
- setResponse(res);
- if (isCompleteResponse(res)) {
- searchSubscription$.current?.unsubscribe();
- setFetchState((prevState) => ({
- ...prevState,
- isRunnning: false,
- isComplete: true,
- }));
- } else if (isErrorResponse(res)) {
- searchSubscription$.current?.unsubscribe();
+ // Submit the search request using the `data.search` service.
+ searchSubscription$.current = data.search
+ .search<
+ IKibanaSearchRequest,
+ IKibanaSearchResponse
+ >(req, {
+ strategy: 'apmCorrelationsSearchStrategy',
+ abortSignal: abortCtrl.current.signal,
+ })
+ .subscribe({
+ next: (res: IKibanaSearchResponse) => {
+ setResponse(res);
+ if (isCompleteResponse(res)) {
+ searchSubscription$.current?.unsubscribe();
+ setFetchState((prevState) => ({
+ ...prevState,
+ isRunnning: false,
+ isComplete: true,
+ }));
+ } else if (isErrorResponse(res)) {
+ searchSubscription$.current?.unsubscribe();
+ setFetchState((prevState) => ({
+ ...prevState,
+ error: (res as unknown) as Error,
+ isRunning: false,
+ }));
+ }
+ },
+ error: (error: Error) => {
setFetchState((prevState) => ({
...prevState,
- error: (res as unknown) as Error,
- setIsRunning: false,
+ error,
+ isRunning: false,
}));
- }
- },
- error: (error: Error) => {
- setFetchState((prevState) => ({
- ...prevState,
- error,
- setIsRunning: false,
- }));
- },
- });
- };
+ },
+ });
+ },
+ [data.search, setFetchState]
+ );
- const cancelFetch = () => {
+ const cancelFetch = useCallback(() => {
searchSubscription$.current?.unsubscribe();
searchSubscription$.current = undefined;
abortCtrl.current.abort();
setFetchState((prevState) => ({
...prevState,
- setIsRunning: false,
+ isRunning: false,
}));
- };
+ }, [setFetchState]);
return {
...fetchState,
diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_latency_correlations_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_latency_correlations_fetcher.ts
index 49f2a279f4931..0b035c6af2354 100644
--- a/x-pack/plugins/apm/public/hooks/use_transaction_latency_correlations_fetcher.ts
+++ b/x-pack/plugins/apm/public/hooks/use_transaction_latency_correlations_fetcher.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { useRef, useState } from 'react';
+import { useCallback, useRef, useState } from 'react';
import type { Subscription } from 'rxjs';
import {
IKibanaSearchRequest,
@@ -14,32 +14,22 @@ import {
isErrorResponse,
} from '../../../../../src/plugins/data/public';
import type {
- HistogramItem,
SearchServiceParams,
- SearchServiceValue,
+ SearchServiceRawResponse,
} from '../../common/search_strategies/correlations/types';
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
import { ApmPluginStartDeps } from '../plugin';
-interface RawResponse {
- percentileThresholdValue?: number;
- took: number;
- values: SearchServiceValue[];
- overallHistogram: HistogramItem[];
- log: string[];
- ccsWarning: boolean;
-}
-
interface TransactionLatencyCorrelationsFetcherState {
error?: Error;
isComplete: boolean;
isRunning: boolean;
loaded: number;
- ccsWarning: RawResponse['ccsWarning'];
- histograms: RawResponse['values'];
- log: RawResponse['log'];
- overallHistogram?: RawResponse['overallHistogram'];
- percentileThresholdValue?: RawResponse['percentileThresholdValue'];
+ ccsWarning: SearchServiceRawResponse['ccsWarning'];
+ histograms: SearchServiceRawResponse['values'];
+ log: SearchServiceRawResponse['log'];
+ overallHistogram?: SearchServiceRawResponse['overallHistogram'];
+ percentileThresholdValue?: SearchServiceRawResponse['percentileThresholdValue'];
timeTook?: number;
total: number;
}
@@ -65,7 +55,9 @@ export const useTransactionLatencyCorrelationsFetcher = () => {
const abortCtrl = useRef(new AbortController());
const searchSubscription$ = useRef();
- function setResponse(response: IKibanaSearchResponse) {
+ function setResponse(
+ response: IKibanaSearchResponse
+ ) {
setFetchState((prevState) => ({
...prevState,
isRunning: response.isRunning || false,
@@ -85,71 +77,86 @@ export const useTransactionLatencyCorrelationsFetcher = () => {
response.rawResponse?.percentileThresholdValue,
}
: {}),
+ // if loading is done but didn't return any data for the overall histogram,
+ // set it to an empty array so the consuming chart component knows loading is done.
+ ...(!response.isRunning &&
+ response.rawResponse?.overallHistogram === undefined
+ ? { overallHistogram: [] }
+ : {}),
}));
}
- const startFetch = (
- params: Omit
- ) => {
- setFetchState((prevState) => ({
- ...prevState,
- error: undefined,
- isComplete: false,
- }));
- searchSubscription$.current?.unsubscribe();
- abortCtrl.current.abort();
- abortCtrl.current = new AbortController();
+ const startFetch = useCallback(
+ (params: Omit) => {
+ setFetchState((prevState) => ({
+ ...prevState,
+ error: undefined,
+ isComplete: false,
+ }));
+ searchSubscription$.current?.unsubscribe();
+ abortCtrl.current.abort();
+ abortCtrl.current = new AbortController();
- const searchServiceParams: SearchServiceParams = {
- ...params,
- analyzeCorrelations: true,
- };
- const req = { params: searchServiceParams };
+ const searchServiceParams: SearchServiceParams = {
+ ...params,
+ analyzeCorrelations: true,
+ };
+ const req = { params: searchServiceParams };
- // Submit the search request using the `data.search` service.
- searchSubscription$.current = data.search
- .search>(req, {
- strategy: 'apmCorrelationsSearchStrategy',
- abortSignal: abortCtrl.current.signal,
- })
- .subscribe({
- next: (res: IKibanaSearchResponse) => {
- setResponse(res);
- if (isCompleteResponse(res)) {
- searchSubscription$.current?.unsubscribe();
+ // Submit the search request using the `data.search` service.
+ searchSubscription$.current = data.search
+ .search<
+ IKibanaSearchRequest,
+ IKibanaSearchResponse
+ >(req, {
+ strategy: 'apmCorrelationsSearchStrategy',
+ abortSignal: abortCtrl.current.signal,
+ })
+ .subscribe({
+ next: (res: IKibanaSearchResponse) => {
+ setResponse(res);
+ if (isCompleteResponse(res)) {
+ searchSubscription$.current?.unsubscribe();
+ setFetchState((prevState) => ({
+ ...prevState,
+ isRunnning: false,
+ isComplete: true,
+ }));
+ } else if (isErrorResponse(res)) {
+ searchSubscription$.current?.unsubscribe();
+ setFetchState((prevState) => ({
+ ...prevState,
+ error: (res as unknown) as Error,
+ isRunning: false,
+ }));
+ }
+ },
+ error: (error: Error) => {
setFetchState((prevState) => ({
...prevState,
- isRunnning: false,
- isComplete: true,
+ error,
+ isRunning: false,
}));
- } else if (isErrorResponse(res)) {
- searchSubscription$.current?.unsubscribe();
- setFetchState((prevState) => ({
- ...prevState,
- error: (res as unknown) as Error,
- setIsRunning: false,
- }));
- }
- },
- error: (error: Error) => {
- setFetchState((prevState) => ({
- ...prevState,
- error,
- setIsRunning: false,
- }));
- },
- });
- };
+ },
+ });
+ },
+ [data.search, setFetchState]
+ );
- const cancelFetch = () => {
+ const cancelFetch = useCallback(() => {
searchSubscription$.current?.unsubscribe();
searchSubscription$.current = undefined;
abortCtrl.current.abort();
setFetchState((prevState) => ({
...prevState,
- setIsRunning: false,
+ // If we didn't receive data for the overall histogram yet
+ // set it to an empty array to indicate loading stopped.
+ ...(prevState.overallHistogram === undefined
+ ? { overallHistogram: [] }
+ : {}),
+ isRunning: false,
}));
- };
+ }, [setFetchState]);
return {
...fetchState,
diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts
index 3601f19ad7051..7f67147a75580 100644
--- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts
+++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts
@@ -16,6 +16,7 @@ import {
import type {
SearchServiceParams,
+ SearchServiceRawResponse,
SearchServiceValue,
} from '../../../../common/search_strategies/correlations/types';
@@ -100,20 +101,22 @@ export const apmCorrelationsSearchStrategyProvider = (
const took = Date.now() - started;
+ const rawResponse: SearchServiceRawResponse = {
+ ccsWarning,
+ log,
+ took,
+ values,
+ percentileThresholdValue,
+ overallHistogram,
+ };
+
return of({
id,
loaded,
total,
isRunning,
isPartial: isRunning,
- rawResponse: {
- ccsWarning,
- log,
- took,
- values,
- percentileThresholdValue,
- overallHistogram,
- },
+ rawResponse,
});
},
cancel: async (id, options, deps) => {