[7.x] [APM] Abort browser requests when appropriate (#89557) (#90011)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Dario Gieselaar 2021-02-02 16:45:46 +01:00 committed by GitHub
parent 9c5e11b07b
commit 9c05ae58b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 511 additions and 423 deletions

View file

@ -57,7 +57,9 @@ export function AnomalyDetectionSetupLink() {
export function MissingJobsAlert({ environment }: { environment?: string }) {
const { data = DEFAULT_DATA, status } = useFetcher(
(callApmApi) =>
callApmApi({ endpoint: `GET /api/apm/settings/anomaly-detection/jobs` }),
callApmApi({
endpoint: `GET /api/apm/settings/anomaly-detection/jobs`,
}),
[],
{ preservePreviousData: false, showToastOnError: false }
);

View file

@ -13,7 +13,6 @@ import { asInteger } from '../../../../common/utils/formatters';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
import { useFetcher } from '../../../hooks/use_fetcher';
import { callApmApi } from '../../../services/rest/createCallApmApi';
import { ChartPreview } from '../chart_preview';
import { EnvironmentField, IsAboveField, ServiceField } from '../fields';
import { getAbsoluteTimeRange } from '../helper';
@ -46,20 +45,23 @@ export function ErrorCountAlertTrigger(props: Props) {
const { threshold, windowSize, windowUnit, environment } = alertParams;
const { data } = useFetcher(() => {
if (windowSize && windowUnit) {
return callApmApi({
endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_count',
params: {
query: {
...getAbsoluteTimeRange(windowSize, windowUnit),
environment,
serviceName,
const { data } = useFetcher(
(callApmApi) => {
if (windowSize && windowUnit) {
return callApmApi({
endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_count',
params: {
query: {
...getAbsoluteTimeRange(windowSize, windowUnit),
environment,
serviceName,
},
},
},
});
}
}, [windowSize, windowUnit, environment, serviceName]);
});
}
},
[windowSize, windowUnit, environment, serviceName]
);
const defaults = {
threshold: 25,

View file

@ -8,7 +8,6 @@ import { i18n } from '@kbn/i18n';
import { map } from 'lodash';
import React from 'react';
import { useParams } from 'react-router-dom';
import { useFetcher } from '../../../../../observability/public';
import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
import { getDurationFormatter } from '../../../../common/utils/formatters';
@ -16,7 +15,7 @@ import { TimeSeries } from '../../../../typings/timeseries';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
import { callApmApi } from '../../../services/rest/createCallApmApi';
import { useFetcher } from '../../../hooks/use_fetcher';
import {
getMaxY,
getResponseTimeTickFormatter,
@ -88,29 +87,32 @@ export function TransactionDurationAlertTrigger(props: Props) {
windowUnit,
} = alertParams;
const { data } = useFetcher(() => {
if (windowSize && windowUnit) {
return callApmApi({
endpoint: 'GET /api/apm/alerts/chart_preview/transaction_duration',
params: {
query: {
...getAbsoluteTimeRange(windowSize, windowUnit),
aggregationType,
environment,
serviceName,
transactionType: alertParams.transactionType,
const { data } = useFetcher(
(callApmApi) => {
if (windowSize && windowUnit) {
return callApmApi({
endpoint: 'GET /api/apm/alerts/chart_preview/transaction_duration',
params: {
query: {
...getAbsoluteTimeRange(windowSize, windowUnit),
aggregationType,
environment,
serviceName,
transactionType: alertParams.transactionType,
},
},
},
});
}
}, [
aggregationType,
environment,
serviceName,
alertParams.transactionType,
windowSize,
windowUnit,
]);
});
}
},
[
aggregationType,
environment,
serviceName,
alertParams.transactionType,
windowSize,
windowUnit,
]
);
const maxY = getMaxY([
{ data: data ?? [] } as TimeSeries<{ x: number; y: number | null }>,

View file

@ -12,7 +12,6 @@ import { useApmServiceContext } from '../../../context/apm_service/use_apm_servi
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
import { useFetcher } from '../../../hooks/use_fetcher';
import { callApmApi } from '../../../services/rest/createCallApmApi';
import { ChartPreview } from '../chart_preview';
import {
EnvironmentField,
@ -54,27 +53,30 @@ export function TransactionErrorRateAlertTrigger(props: Props) {
const thresholdAsPercent = (threshold ?? 0) / 100;
const { data } = useFetcher(() => {
if (windowSize && windowUnit) {
return callApmApi({
endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_rate',
params: {
query: {
...getAbsoluteTimeRange(windowSize, windowUnit),
environment,
serviceName,
transactionType: alertParams.transactionType,
const { data } = useFetcher(
(callApmApi) => {
if (windowSize && windowUnit) {
return callApmApi({
endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_rate',
params: {
query: {
...getAbsoluteTimeRange(windowSize, windowUnit),
environment,
serviceName,
transactionType: alertParams.transactionType,
},
},
},
});
}
}, [
alertParams.transactionType,
environment,
serviceName,
windowSize,
windowUnit,
]);
});
}
},
[
alertParams.transactionType,
environment,
serviceName,
windowSize,
windowUnit,
]
);
if (serviceName && !transactionTypes.length) {
return null;

View file

@ -25,10 +25,7 @@ import {
} from '@elastic/eui';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import {
APIReturnType,
callApmApi,
} from '../../../services/rest/createCallApmApi';
import { APIReturnType } from '../../../services/rest/createCallApmApi';
import { px } from '../../../style/variables';
import { SignificantTermsTable } from './SignificantTermsTable';
import { ChartContainer } from '../../shared/charts/chart_container';
@ -65,32 +62,35 @@ export function ErrorCorrelations() {
const { urlParams, uiFilters } = useUrlParams();
const { transactionName, transactionType, start, end } = urlParams;
const { data, status } = useFetcher(() => {
if (start && end) {
return callApmApi({
endpoint: 'GET /api/apm/correlations/failed_transactions',
params: {
query: {
serviceName,
transactionName,
transactionType,
start,
end,
uiFilters: JSON.stringify(uiFilters),
fieldNames: fieldNames.map((field) => field.label).join(','),
const { data, status } = useFetcher(
(callApmApi) => {
if (start && end) {
return callApmApi({
endpoint: 'GET /api/apm/correlations/failed_transactions',
params: {
query: {
serviceName,
transactionName,
transactionType,
start,
end,
uiFilters: JSON.stringify(uiFilters),
fieldNames: fieldNames.map((field) => field.label).join(','),
},
},
},
});
}
}, [
serviceName,
start,
end,
transactionName,
transactionType,
uiFilters,
fieldNames,
]);
});
}
},
[
serviceName,
start,
end,
transactionName,
transactionType,
uiFilters,
fieldNames,
]
);
return (
<>

View file

@ -26,10 +26,7 @@ import {
import { getDurationFormatter } from '../../../../common/utils/formatters';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import {
APIReturnType,
callApmApi,
} from '../../../services/rest/createCallApmApi';
import { APIReturnType } from '../../../services/rest/createCallApmApi';
import { SignificantTermsTable } from './SignificantTermsTable';
import { ChartContainer } from '../../shared/charts/chart_container';
@ -65,34 +62,37 @@ export function LatencyCorrelations() {
const { urlParams, uiFilters } = useUrlParams();
const { transactionName, transactionType, start, end } = urlParams;
const { data, status } = useFetcher(() => {
if (start && end) {
return callApmApi({
endpoint: 'GET /api/apm/correlations/slow_transactions',
params: {
query: {
serviceName,
transactionName,
transactionType,
start,
end,
uiFilters: JSON.stringify(uiFilters),
durationPercentile,
fieldNames: fieldNames.map((field) => field.label).join(','),
const { data, status } = useFetcher(
(callApmApi) => {
if (start && end) {
return callApmApi({
endpoint: 'GET /api/apm/correlations/slow_transactions',
params: {
query: {
serviceName,
transactionName,
transactionType,
start,
end,
uiFilters: JSON.stringify(uiFilters),
durationPercentile,
fieldNames: fieldNames.map((field) => field.label).join(','),
},
},
},
});
}
}, [
serviceName,
start,
end,
transactionName,
transactionType,
uiFilters,
durationPercentile,
fieldNames,
]);
});
}
},
[
serviceName,
start,
end,
transactionName,
transactionType,
uiFilters,
durationPercentile,
fieldNames,
]
);
return (
<>

View file

@ -23,7 +23,6 @@ import { useTrackPageview } from '../../../../../observability/public';
import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n';
import { useFetcher } from '../../../hooks/use_fetcher';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { callApmApi } from '../../../services/rest/createCallApmApi';
import { fontFamilyCode, fontSizes, px, units } from '../../../style/variables';
import { ApmHeader } from '../../shared/ApmHeader';
import { SearchBar } from '../../shared/search_bar';
@ -70,24 +69,27 @@ export function ErrorGroupDetails({ location, match }: ErrorGroupDetailsProps) {
const { urlParams, uiFilters } = useUrlParams();
const { start, end } = urlParams;
const { data: errorGroupData } = useFetcher(() => {
if (start && end) {
return callApmApi({
endpoint: 'GET /api/apm/services/{serviceName}/errors/{groupId}',
params: {
path: {
serviceName,
groupId,
const { data: errorGroupData } = useFetcher(
(callApmApi) => {
if (start && end) {
return callApmApi({
endpoint: 'GET /api/apm/services/{serviceName}/errors/{groupId}',
params: {
path: {
serviceName,
groupId,
},
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters),
},
},
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters),
},
},
});
}
}, [serviceName, start, end, groupId, uiFilters]);
});
}
},
[serviceName, start, end, groupId, uiFilters]
);
const { errorDistributionData } = useErrorGroupDistributionFetcher({
serviceName,

View file

@ -18,7 +18,6 @@ import { useTrackPageview } from '../../../../../observability/public';
import { Projection } from '../../../../common/projections';
import { useFetcher } from '../../../hooks/use_fetcher';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { callApmApi } from '../../../services/rest/createCallApmApi';
import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { SearchBar } from '../../shared/search_bar';
import { ErrorDistribution } from '../ErrorGroupDetails/Distribution';
@ -37,27 +36,30 @@ function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) {
groupId: undefined,
});
const { data: errorGroupListData } = useFetcher(() => {
const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc';
const { data: errorGroupListData } = useFetcher(
(callApmApi) => {
const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc';
if (start && end) {
return callApmApi({
endpoint: 'GET /api/apm/services/{serviceName}/errors',
params: {
path: {
serviceName,
if (start && end) {
return callApmApi({
endpoint: 'GET /api/apm/services/{serviceName}/errors',
params: {
path: {
serviceName,
},
query: {
start,
end,
sortField,
sortDirection: normalizedSortDirection,
uiFilters: JSON.stringify(uiFilters),
},
},
query: {
start,
end,
sortField,
sortDirection: normalizedSortDirection,
uiFilters: JSON.stringify(uiFilters),
},
},
});
}
}, [serviceName, start, end, sortField, sortDirection, uiFilters]);
});
}
},
[serviceName, start, end, sortField, sortDirection, uiFilters]
);
useTrackPageview({
app: 'apm',

View file

@ -21,6 +21,7 @@ export const fetchUxOverviewDate = async ({
}: FetchDataParams): Promise<UxFetchDataResponse> => {
const data = await callApmApi({
endpoint: 'GET /api/apm/rum-client/web-core-vitals',
signal: null,
params: {
query: {
start: new Date(absoluteTime.start).toISOString(),
@ -41,6 +42,7 @@ export async function hasRumData({
}: HasDataParams): Promise<UXHasDataResponse> {
return await callApmApi({
endpoint: 'GET /api/apm/observability_overview/has_rum_data',
signal: null,
params: {
query: {
start: new Date(absoluteTime.start).toISOString(),

View file

@ -17,7 +17,6 @@ import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { useLicenseContext } from '../../../context/license/use_license_context';
import { useTheme } from '../../../hooks/use_theme';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { callApmApi } from '../../../services/rest/createCallApmApi';
import { DatePicker } from '../../shared/DatePicker';
import { LicensePrompt } from '../../shared/LicensePrompt';
import { Controls } from './Controls';
@ -86,28 +85,31 @@ export function ServiceMap({
const license = useLicenseContext();
const { urlParams } = useUrlParams();
const { data = { elements: [] }, status, error } = useFetcher(() => {
// When we don't have a license or a valid license, don't make the request.
if (!license || !isActivePlatinumLicense(license)) {
return;
}
const { data = { elements: [] }, status, error } = useFetcher(
(callApmApi) => {
// When we don't have a license or a valid license, don't make the request.
if (!license || !isActivePlatinumLicense(license)) {
return;
}
const { start, end, environment } = urlParams;
if (start && end) {
return callApmApi({
isCachable: false,
endpoint: 'GET /api/apm/service-map',
params: {
query: {
start,
end,
environment,
serviceName,
const { start, end, environment } = urlParams;
if (start && end) {
return callApmApi({
isCachable: false,
endpoint: 'GET /api/apm/service-map',
params: {
query: {
start,
end,
environment,
serviceName,
},
},
},
});
}
}, [license, serviceName, urlParams]);
});
}
},
[license, serviceName, urlParams]
);
const { ref, height } = useRefDimensions();

View file

@ -26,6 +26,7 @@ export async function saveConfig({
try {
await callApmApi({
endpoint: 'PUT /api/apm/settings/agent-configuration',
signal: null,
params: {
query: { overwrite: isEditMode },
body: {

View file

@ -73,6 +73,7 @@ async function deleteConfig(
try {
await callApmApi({
endpoint: 'DELETE /api/apm/settings/agent-configuration',
signal: null,
params: {
body: {
service: {

View file

@ -74,6 +74,7 @@ async function saveApmIndices({
}) {
await callApmApi({
endpoint: 'POST /api/apm/settings/apm-indices/save',
signal: null,
params: {
body: apmIndices,
},

View file

@ -48,6 +48,7 @@ async function deleteConfig(
try {
await callApmApi({
endpoint: 'DELETE /api/apm/settings/custom_links/{id}',
signal: null,
params: {
path: { id: customLinkId },
},

View file

@ -31,6 +31,7 @@ interface Props {
const fetchTransaction = debounce(
async (filters: Filter[], callback: (transaction: Transaction) => void) => {
const transaction = await callApmApi({
signal: null,
endpoint: 'GET /api/apm/settings/custom_links/transaction',
params: { query: convertFiltersToQuery(filters) },
});

View file

@ -35,6 +35,7 @@ export async function saveCustomLink({
if (id) {
await callApmApi({
endpoint: 'PUT /api/apm/settings/custom_links/{id}',
signal: null,
params: {
path: { id },
body: customLink,
@ -43,6 +44,7 @@ export async function saveCustomLink({
} else {
await callApmApi({
endpoint: 'POST /api/apm/settings/custom_links',
signal: null,
params: {
body: customLink,
},

View file

@ -28,6 +28,7 @@ export async function createJobs({
try {
await callApmApi({
endpoint: 'POST /api/apm/settings/anomaly-detection/jobs',
signal: null,
params: {
body: { environments },
},

View file

@ -8,7 +8,9 @@ import { useFetcher } from '../../../hooks/use_fetcher';
export function useAnomalyDetectionJobsFetcher() {
const { data, status } = useFetcher(
(callApmApi) =>
callApmApi({ endpoint: `GET /api/apm/settings/anomaly-detection/jobs` }),
callApmApi({
endpoint: `GET /api/apm/settings/anomaly-detection/jobs`,
}),
[],
{ showToastOnError: false }
);

View file

@ -26,7 +26,6 @@ import {
import { ServiceDependencyItem } from '../../../../../server/lib/services/get_service_dependencies';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { callApmApi } from '../../../../services/rest/createCallApmApi';
import { px, unit } from '../../../../style/variables';
import { AgentIcon } from '../../../shared/AgentIcon';
import { SparkPlot } from '../../../shared/charts/spark_plot';
@ -167,26 +166,29 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) {
},
];
const { data = [], status } = useFetcher(() => {
if (!start || !end) {
return;
}
const { data = [], status } = useFetcher(
(callApmApi) => {
if (!start || !end) {
return;
}
return callApmApi({
endpoint: 'GET /api/apm/services/{serviceName}/dependencies',
params: {
path: {
serviceName,
return callApmApi({
endpoint: 'GET /api/apm/services/{serviceName}/dependencies',
params: {
path: {
serviceName,
},
query: {
start,
end,
environment: environment || ENVIRONMENT_ALL.value,
numBuckets: 20,
},
},
query: {
start,
end,
environment: environment || ENVIRONMENT_ALL.value,
numBuckets: 20,
},
},
});
}, [start, end, serviceName, environment]);
});
},
[start, end, serviceName, environment]
);
// need top-level sortable fields for the managed table
const items = data.map((item) => ({

View file

@ -16,7 +16,6 @@ import { asInteger } from '../../../../../common/utils/formatters';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { callApmApi } from '../../../../services/rest/createCallApmApi';
import { px, unit } from '../../../../style/variables';
import { SparkPlot } from '../../../shared/charts/spark_plot';
import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink';
@ -140,50 +139,53 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
},
},
status,
} = useFetcher(() => {
if (!start || !end || !transactionType) {
return;
}
} = useFetcher(
(callApmApi) => {
if (!start || !end || !transactionType) {
return;
}
return callApmApi({
endpoint: 'GET /api/apm/services/{serviceName}/error_groups',
params: {
path: { serviceName },
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters),
size: PAGE_SIZE,
numBuckets: 20,
pageIndex: tableOptions.pageIndex,
sortField: tableOptions.sort.field,
sortDirection: tableOptions.sort.direction,
transactionType,
},
},
}).then((response) => {
return {
items: response.error_groups,
totalItemCount: response.total_error_groups,
tableOptions: {
pageIndex: tableOptions.pageIndex,
sort: {
field: tableOptions.sort.field,
direction: tableOptions.sort.direction,
return callApmApi({
endpoint: 'GET /api/apm/services/{serviceName}/error_groups',
params: {
path: { serviceName },
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters),
size: PAGE_SIZE,
numBuckets: 20,
pageIndex: tableOptions.pageIndex,
sortField: tableOptions.sort.field,
sortDirection: tableOptions.sort.direction,
transactionType,
},
},
};
});
}, [
start,
end,
serviceName,
uiFilters,
tableOptions.pageIndex,
tableOptions.sort.field,
tableOptions.sort.direction,
transactionType,
]);
}).then((response) => {
return {
items: response.error_groups,
totalItemCount: response.total_error_groups,
tableOptions: {
pageIndex: tableOptions.pageIndex,
sort: {
field: tableOptions.sort.field,
direction: tableOptions.sort.direction,
},
},
};
});
},
[
start,
end,
serviceName,
uiFilters,
tableOptions.pageIndex,
tableOptions.sort.field,
tableOptions.sort.direction,
transactionType,
]
);
const {
items,

View file

@ -9,7 +9,6 @@ import React from 'react';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useFetcher } from '../../../hooks/use_fetcher';
import { callApmApi } from '../../../services/rest/createCallApmApi';
import { InstancesLatencyDistributionChart } from '../../shared/charts/instances_latency_distribution_chart';
import { ServiceOverviewInstancesTable } from './service_overview_instances_table';
@ -29,28 +28,31 @@ export function ServiceOverviewInstancesChartAndTable({
uiFilters,
} = useUrlParams();
const { data = [], status } = useFetcher(() => {
if (!start || !end || !transactionType) {
return;
}
const { data = [], status } = useFetcher(
(callApmApi) => {
if (!start || !end || !transactionType) {
return;
}
return callApmApi({
endpoint:
'GET /api/apm/services/{serviceName}/service_overview_instances',
params: {
path: {
serviceName,
return callApmApi({
endpoint:
'GET /api/apm/services/{serviceName}/service_overview_instances',
params: {
path: {
serviceName,
},
query: {
start,
end,
transactionType,
uiFilters: JSON.stringify(uiFilters),
numBuckets: 20,
},
},
query: {
start,
end,
transactionType,
uiFilters: JSON.stringify(uiFilters),
numBuckets: 20,
},
},
});
}, [start, end, serviceName, transactionType, uiFilters]);
});
},
[start, end, serviceName, transactionType, uiFilters]
);
return (
<>

View file

@ -13,7 +13,6 @@ import { useFetcher } from '../../../hooks/use_fetcher';
import { useTheme } from '../../../hooks/use_theme';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { callApmApi } from '../../../services/rest/createCallApmApi';
import { TimeseriesChart } from '../../shared/charts/timeseries_chart';
export function ServiceOverviewThroughputChart({
@ -27,24 +26,27 @@ export function ServiceOverviewThroughputChart({
const { transactionType } = useApmServiceContext();
const { start, end } = urlParams;
const { data, status } = useFetcher(() => {
if (serviceName && transactionType && start && end) {
return callApmApi({
endpoint: 'GET /api/apm/services/{serviceName}/throughput',
params: {
path: {
serviceName,
const { data, status } = useFetcher(
(callApmApi) => {
if (serviceName && transactionType && start && end) {
return callApmApi({
endpoint: 'GET /api/apm/services/{serviceName}/throughput',
params: {
path: {
serviceName,
},
query: {
start,
end,
transactionType,
uiFilters: JSON.stringify(uiFilters),
},
},
query: {
start,
end,
transactionType,
uiFilters: JSON.stringify(uiFilters),
},
},
});
}
}, [serviceName, start, end, uiFilters, transactionType]);
});
}
},
[serviceName, start, end, uiFilters, transactionType]
);
return (
<EuiPanel>

View file

@ -23,10 +23,7 @@ import {
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import {
APIReturnType,
callApmApi,
} from '../../../../services/rest/createCallApmApi';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
import { px, unit } from '../../../../style/variables';
import { SparkPlot } from '../../../shared/charts/spark_plot';
import { ImpactBar } from '../../../shared/ImpactBar';
@ -110,53 +107,56 @@ export function ServiceOverviewTransactionsTable(props: Props) {
},
},
status,
} = useFetcher(() => {
if (!start || !end || !latencyAggregationType || !transactionType) {
return;
}
} = useFetcher(
(callApmApi) => {
if (!start || !end || !latencyAggregationType || !transactionType) {
return;
}
return callApmApi({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/groups/overview',
params: {
path: { serviceName },
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters),
size: PAGE_SIZE,
numBuckets: 20,
pageIndex: tableOptions.pageIndex,
sortField: tableOptions.sort.field,
sortDirection: tableOptions.sort.direction,
transactionType,
latencyAggregationType: latencyAggregationType as LatencyAggregationType,
},
},
}).then((response) => {
return {
items: response.transactionGroups,
totalItemCount: response.totalTransactionGroups,
tableOptions: {
pageIndex: tableOptions.pageIndex,
sort: {
field: tableOptions.sort.field,
direction: tableOptions.sort.direction,
return callApmApi({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/groups/overview',
params: {
path: { serviceName },
query: {
start,
end,
uiFilters: JSON.stringify(uiFilters),
size: PAGE_SIZE,
numBuckets: 20,
pageIndex: tableOptions.pageIndex,
sortField: tableOptions.sort.field,
sortDirection: tableOptions.sort.direction,
transactionType,
latencyAggregationType: latencyAggregationType as LatencyAggregationType,
},
},
};
});
}, [
serviceName,
start,
end,
uiFilters,
tableOptions.pageIndex,
tableOptions.sort.field,
tableOptions.sort.direction,
transactionType,
latencyAggregationType,
]);
}).then((response) => {
return {
items: response.transactionGroups,
totalItemCount: response.totalTransactionGroups,
tableOptions: {
pageIndex: tableOptions.pageIndex,
sort: {
field: tableOptions.sort.field,
direction: tableOptions.sort.direction,
},
},
};
});
},
[
serviceName,
start,
end,
uiFilters,
tableOptions.pageIndex,
tableOptions.sort.field,
tableOptions.sort.direction,
transactionType,
latencyAggregationType,
]
);
const {
items,

View file

@ -12,7 +12,6 @@ import { asPercent } from '../../../../../common/utils/formatters';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { useTheme } from '../../../../hooks/use_theme';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { callApmApi } from '../../../../services/rest/createCallApmApi';
import { TimeseriesChart } from '../timeseries_chart';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
@ -35,26 +34,29 @@ export function TransactionErrorRateChart({
const { transactionType } = useApmServiceContext();
const { start, end, transactionName } = urlParams;
const { data, status } = useFetcher(() => {
if (transactionType && serviceName && start && end) {
return callApmApi({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/charts/error_rate',
params: {
path: {
serviceName,
const { data, status } = useFetcher(
(callApmApi) => {
if (transactionType && serviceName && start && end) {
return callApmApi({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/charts/error_rate',
params: {
path: {
serviceName,
},
query: {
start,
end,
transactionType,
transactionName,
uiFilters: JSON.stringify(uiFilters),
},
},
query: {
start,
end,
transactionType,
transactionName,
uiFilters: JSON.stringify(uiFilters),
},
},
});
}
}, [serviceName, start, end, uiFilters, transactionType, transactionName]);
});
}
},
[serviceName, start, end, uiFilters, transactionType, transactionName]
);
const errorRates = data?.transactionErrorRate || [];

View file

@ -9,7 +9,6 @@ import { useParams } from 'react-router-dom';
import { Annotation } from '../../../common/annotations';
import { useFetcher } from '../../hooks/use_fetcher';
import { useUrlParams } from '../url_params_context/use_url_params';
import { callApmApi } from '../../services/rest/createCallApmApi';
export const AnnotationsContext = createContext({ annotations: [] } as {
annotations: Annotation[];
@ -27,23 +26,26 @@ export function AnnotationsContextProvider({
const { start, end } = urlParams;
const { environment } = uiFilters;
const { data = INITIAL_STATE } = useFetcher(() => {
if (start && end && serviceName) {
return callApmApi({
endpoint: 'GET /api/apm/services/{serviceName}/annotation/search',
params: {
path: {
serviceName,
const { data = INITIAL_STATE } = useFetcher(
(callApmApi) => {
if (start && end && serviceName) {
return callApmApi({
endpoint: 'GET /api/apm/services/{serviceName}/annotation/search',
params: {
path: {
serviceName,
},
query: {
start,
end,
environment,
},
},
query: {
start,
end,
environment,
},
},
});
}
}, [start, end, environment, serviceName]);
});
}
},
[start, end, environment, serviceName]
);
return <AnnotationsContext.Provider value={data} children={children} />;
}

View file

@ -16,7 +16,6 @@ import {
} from '../../server/lib/ui_filters/local_ui_filters/config';
import { fromQuery, toQuery } from '../components/shared/Links/url_helpers';
import { removeUndefinedProps } from '../context/url_params_context/helpers';
import { useCallApi } from './useCallApi';
import { useFetcher } from './use_fetcher';
import { useUrlParams } from '../context/url_params_context/use_url_params';
import { LocalUIFilterName } from '../../common/ui_filter';
@ -43,7 +42,6 @@ export function useLocalUIFilters({
}) {
const history = useHistory();
const { uiFilters, urlParams } = useUrlParams();
const callApi = useCallApi();
const values = pickKeys(uiFilters, ...filterNames);
@ -69,30 +67,34 @@ export function useLocalUIFilters({
});
};
const { data = getInitialData(filterNames), status } = useFetcher(() => {
if (shouldFetch) {
return callApi<LocalUIFiltersAPIResponse>({
method: 'GET',
pathname: `/api/apm/ui_filters/local_filters/${projection}`,
query: {
uiFilters: JSON.stringify(uiFilters),
start: urlParams.start,
end: urlParams.end,
filterNames: JSON.stringify(filterNames),
...params,
},
});
}
}, [
callApi,
projection,
uiFilters,
urlParams.start,
urlParams.end,
filterNames,
params,
shouldFetch,
]);
const { data = getInitialData(filterNames), status } = useFetcher(
(callApmApi) => {
if (shouldFetch && urlParams.start && urlParams.end) {
return callApmApi({
endpoint: `GET /api/apm/ui_filters/local_filters/${projection}` as const,
params: {
query: {
uiFilters: JSON.stringify(uiFilters),
start: urlParams.start,
end: urlParams.end,
// type expects string constants, but we have to send it as json
filterNames: JSON.stringify(filterNames) as any,
...params,
},
},
});
}
},
[
projection,
uiFilters,
urlParams.start,
urlParams.end,
filterNames,
params,
shouldFetch,
]
);
const filters = data.map((filter) => ({
...filter,

View file

@ -10,7 +10,6 @@ import {
ENVIRONMENT_ALL,
ENVIRONMENT_NOT_DEFINED,
} from '../../common/environment_filter_values';
import { callApmApi } from '../services/rest/createCallApmApi';
function getEnvironmentOptions(environments: string[]) {
const environmentOptions = environments
@ -32,20 +31,23 @@ export function useEnvironmentsFetcher({
start?: string;
end?: string;
}) {
const { data: environments = [], status = 'loading' } = useFetcher(() => {
if (start && end) {
return callApmApi({
endpoint: 'GET /api/apm/ui_filters/environments',
params: {
query: {
start,
end,
serviceName,
const { data: environments = [], status = 'loading' } = useFetcher(
(callApmApi) => {
if (start && end) {
return callApmApi({
endpoint: 'GET /api/apm/ui_filters/environments',
params: {
query: {
start,
end,
serviceName,
},
},
},
});
}
}, [start, end, serviceName]);
});
}
},
[start, end, serviceName]
);
const environmentOptions = useMemo(
() => getEnvironmentOptions(environments),

View file

@ -8,7 +8,10 @@ import { i18n } from '@kbn/i18n';
import React, { useEffect, useMemo, useState } from 'react';
import { IHttpFetchError } from 'src/core/public';
import { toMountPoint } from '../../../../../src/plugins/kibana_react/public';
import { APMClient, callApmApi } from '../services/rest/createCallApmApi';
import {
callApmApi,
AutoAbortedAPMClient,
} from '../services/rest/createCallApmApi';
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
export enum FETCH_STATUS {
@ -39,6 +42,14 @@ function getDetailsFromErrorResponse(error: IHttpFetchError) {
);
}
const createAutoAbortedAPMClient = (
signal: AbortSignal
): AutoAbortedAPMClient => {
return ((options: Parameters<AutoAbortedAPMClient>[0]) => {
return callApmApi({ ...options, signal });
}) as AutoAbortedAPMClient;
};
// fetcher functions can return undefined OR a promise. Previously we had a more simple type
// but it led to issues when using object destructuring with default values
type InferResponseType<TReturn> = Exclude<TReturn, undefined> extends Promise<
@ -48,7 +59,7 @@ type InferResponseType<TReturn> = Exclude<TReturn, undefined> extends Promise<
: unknown;
export function useFetcher<TReturn>(
fn: (callApmApi: APMClient) => TReturn,
fn: (callApmApi: AutoAbortedAPMClient) => TReturn,
fnDeps: any[],
options: {
preservePreviousData?: boolean;
@ -66,10 +77,16 @@ export function useFetcher<TReturn>(
const [counter, setCounter] = useState(0);
useEffect(() => {
let didCancel = false;
let controller: AbortController = new AbortController();
async function doFetch() {
const promise = fn(callApmApi);
controller.abort();
controller = new AbortController();
const signal = controller.signal;
const promise = fn(createAutoAbortedAPMClient(signal));
// if `fn` doesn't return a promise it is a signal that data fetching was not initiated.
// This can happen if the data fetching is conditional (based on certain inputs).
// In these cases it is not desirable to invoke the global loading spinner, or change the status to success
@ -85,7 +102,11 @@ export function useFetcher<TReturn>(
try {
const data = await promise;
if (!didCancel) {
// when http fetches are aborted, the promise will be rejected
// and this code is never reached. For async operations that are
// not cancellable, we need to check whether the signal was
// aborted before updating the result.
if (!signal.aborted) {
setResult({
data,
status: FETCH_STATUS.SUCCESS,
@ -95,7 +116,7 @@ export function useFetcher<TReturn>(
} catch (e) {
const err = e as Error | IHttpFetchError;
if (!didCancel) {
if (!signal.aborted) {
const errorDetails =
'response' in err ? getDetailsFromErrorResponse(err) : err.message;
@ -130,7 +151,7 @@ export function useFetcher<TReturn>(
doFetch();
return () => {
didCancel = true;
controller.abort();
};
/* eslint-disable react-hooks/exhaustive-deps */
}, [

View file

@ -20,6 +20,7 @@ export const fetchObservabilityOverviewPageData = async ({
}: FetchDataParams): Promise<ApmFetchDataResponse> => {
const data = await callApmApi({
endpoint: 'GET /api/apm/observability_overview',
signal: null,
params: {
query: {
start: new Date(absoluteTime.start).toISOString(),
@ -59,5 +60,6 @@ export const fetchObservabilityOverviewPageData = async ({
export async function hasData() {
return await callApmApi({
endpoint: 'GET /api/apm/observability_overview/has_data',
signal: null,
});
}

View file

@ -87,5 +87,6 @@ function isCachable(fetchOptions: FetchOptions) {
// order the options object to make sure that two objects with the same arguments, produce produce the
// same cache key regardless of the order of properties
function getCacheKey(options: FetchOptions) {
return hash(options);
const { pathname, method, body, query, headers } = options;
return hash({ pathname, method, body, query, headers });
}

View file

@ -12,11 +12,14 @@ import { APMAPI } from '../../../server/routes/create_apm_api';
import { Client } from '../../../server/routes/typings';
export type APMClient = Client<APMAPI['_S']>;
export type AutoAbortedAPMClient = Client<APMAPI['_S'], { abortable: false }>;
export type APMClientOptions = Omit<
FetchOptions,
'query' | 'body' | 'pathname'
'query' | 'body' | 'pathname' | 'signal'
> & {
endpoint: string;
signal: AbortSignal | null;
params?: {
body?: any;
query?: any;

View file

@ -9,11 +9,13 @@ import { callApmApi } from './createCallApmApi';
export const createStaticIndexPattern = async () => {
return await callApmApi({
endpoint: 'POST /api/apm/index_pattern/static',
signal: null,
});
};
export const getApmIndexPatternTitle = async () => {
return await callApmApi({
endpoint: 'GET /api/apm/index_pattern/title',
signal: null,
});
};

View file

@ -65,10 +65,12 @@ describe('createApmEventClient', () => {
await new Promise((resolve) => {
setTimeout(() => {
incomingRequest.on('abort', () => {
setTimeout(() => {
resolve(undefined);
}, 0);
});
incomingRequest.abort();
setTimeout(() => {
resolve(undefined);
}, 0);
}, 50);
});

View file

@ -92,7 +92,7 @@ function getMockRequest() {
url: '',
events: {
aborted$: {
subscribe: jest.fn(),
subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }),
},
},
} as unknown) as KibanaRequest;

View file

@ -10,6 +10,7 @@ import * as t from 'io-ts';
import { PathReporter } from 'io-ts/lib/PathReporter';
import { isLeft } from 'fp-ts/lib/Either';
import { KibanaResponseFactory, RouteRegistrar } from 'src/core/server';
import { RequestAbortedError } from '@elastic/elasticsearch/lib/errors';
import { merge } from '../../../common/runtime_types/merge';
import { strictKeysRt } from '../../../common/runtime_types/strict_keys_rt';
import { APMConfig } from '../..';
@ -132,6 +133,15 @@ export function createApi() {
if (Boom.isBoom(error)) {
return convertBoomToKibanaResponse(error, response);
}
if (error instanceof RequestAbortedError) {
return response.custom({
statusCode: 499,
body: {
message: 'Client closed request',
},
});
}
throw error;
}
}

View file

@ -131,15 +131,20 @@ type MaybeOptional<T extends { params: Record<string, any> }> = RequiredKeys<
? { params?: T['params'] }
: { params: T['params'] };
export type Client<TRouteState> = <
TEndpoint extends keyof TRouteState & string
>(
options: Omit<FetchOptions, 'query' | 'body' | 'pathname' | 'method'> & {
export type Client<
TRouteState,
TOptions extends { abortable: boolean } = { abortable: true }
> = <TEndpoint extends keyof TRouteState & string>(
options: Omit<
FetchOptions,
'query' | 'body' | 'pathname' | 'method' | 'signal'
> & {
forceCache?: boolean;
endpoint: TEndpoint;
} & (TRouteState[TEndpoint] extends { params: t.Any }
? MaybeOptional<{ params: t.TypeOf<TRouteState[TEndpoint]['params']> }>
: {})
: {}) &
(TOptions extends { abortable: true } ? { signal: AbortSignal | null } : {})
) => Promise<
TRouteState[TEndpoint] extends { ret: any }
? TRouteState[TEndpoint]['ret']