[8.17] [APM] Do not rely on rendered items directly to update dependent requests (#216312) (#217162)

# Backport

This will backport the following commits from `main` to `8.17`:
- [[APM] Do not rely on rendered items directly to update dependent
requests (#216312)](https://github.com/elastic/kibana/pull/216312)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Gonçalo Rica Pais da
Silva","email":"goncalo.rica@elastic.co"},"sourceCommit":{"committedDate":"2025-04-03T18:23:09Z","message":"[APM]
Do not rely on rendered items directly to update dependent requests
(#216312)\n\n## Summary\n\nThis PR fixes some wonky request handling for
particular cases. In some\ncases, when the page updates with some new
criteria/filters, requests\nthat were being managed by `ManagedTable`
would fire at least twice,\nwith the first one getting aborted and then
resent. This PR removes that\nbehaviour by not storing the dependent
data in a `renderedItems` state\nand instead depending directly on the
request data itself and storing\ninstead the indices of the rendered
items. This removes the edge-case\nwhere `renderedItems` would cause the
affected requests from firing\nmultiple times, due to object equality
not being the same for the\nrendered items array between
renders.\n\nCloses #216144\n\n## How to test\n\n* Go to Observability ->
Applications -> Service Inventory\n* Select a service with more than one
environment\n* Go to Errors tab and open the browser dev tools\n* Change
the environment on the service to update the errors tab\n\n**Expected
behaviour**: The `detailed_statistics` request should only\nfire once,
both on page load and on update (such as changing the
service\nenvironment).\n\nThis should apply to the Service Inventory
page as well, and anything\nmaking use of the `TransactionsTable`
component.","sha":"0cff949bb444002efdf12f55eba6b2970f9c6b4f","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","v9.0.0","apm","backport:prev-major","Team:obs-ux-infra_services","v8.17.0","v9.1.0"],"title":"[APM]
Do not rely on rendered items directly to update dependent
requests","number":216312,"url":"https://github.com/elastic/kibana/pull/216312","mergeCommit":{"message":"[APM]
Do not rely on rendered items directly to update dependent requests
(#216312)\n\n## Summary\n\nThis PR fixes some wonky request handling for
particular cases. In some\ncases, when the page updates with some new
criteria/filters, requests\nthat were being managed by `ManagedTable`
would fire at least twice,\nwith the first one getting aborted and then
resent. This PR removes that\nbehaviour by not storing the dependent
data in a `renderedItems` state\nand instead depending directly on the
request data itself and storing\ninstead the indices of the rendered
items. This removes the edge-case\nwhere `renderedItems` would cause the
affected requests from firing\nmultiple times, due to object equality
not being the same for the\nrendered items array between
renders.\n\nCloses #216144\n\n## How to test\n\n* Go to Observability ->
Applications -> Service Inventory\n* Select a service with more than one
environment\n* Go to Errors tab and open the browser dev tools\n* Change
the environment on the service to update the errors tab\n\n**Expected
behaviour**: The `detailed_statistics` request should only\nfire once,
both on page load and on update (such as changing the
service\nenvironment).\n\nThis should apply to the Service Inventory
page as well, and anything\nmaking use of the `TransactionsTable`
component.","sha":"0cff949bb444002efdf12f55eba6b2970f9c6b4f"}},"sourceBranch":"main","suggestedTargetBranches":["9.0","8.17"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.17","label":"v8.17.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/216312","number":216312,"mergeCommit":{"message":"[APM]
Do not rely on rendered items directly to update dependent requests
(#216312)\n\n## Summary\n\nThis PR fixes some wonky request handling for
particular cases. In some\ncases, when the page updates with some new
criteria/filters, requests\nthat were being managed by `ManagedTable`
would fire at least twice,\nwith the first one getting aborted and then
resent. This PR removes that\nbehaviour by not storing the dependent
data in a `renderedItems` state\nand instead depending directly on the
request data itself and storing\ninstead the indices of the rendered
items. This removes the edge-case\nwhere `renderedItems` would cause the
affected requests from firing\nmultiple times, due to object equality
not being the same for the\nrendered items array between
renders.\n\nCloses #216144\n\n## How to test\n\n* Go to Observability ->
Applications -> Service Inventory\n* Select a service with more than one
environment\n* Go to Errors tab and open the browser dev tools\n* Change
the environment on the service to update the errors tab\n\n**Expected
behaviour**: The `detailed_statistics` request should only\nfire once,
both on page load and on update (such as changing the
service\nenvironment).\n\nThis should apply to the Service Inventory
page as well, and anything\nmaking use of the `TransactionsTable`
component.","sha":"0cff949bb444002efdf12f55eba6b2970f9c6b4f"}}]}]
BACKPORT-->
This commit is contained in:
Gonçalo Rica Pais da Silva 2025-04-04 15:23:23 +02:00 committed by GitHub
parent 46091ee20b
commit 4e5a545a0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 98 additions and 53 deletions

View file

@ -19,12 +19,13 @@ import { ChartType, getTimeSeriesColor } from '../../../shared/charts/helper/get
import { SparkPlot } from '../../../shared/charts/spark_plot';
import { ErrorDetailLink } from '../../../shared/links/apm/error_detail_link';
import { ErrorOverviewLink } from '../../../shared/links/apm/error_overview_link';
import {
import type {
ITableColumn,
ManagedTable,
TableOptions,
TableSearchBar,
VisibleItemsStartEnd,
} from '../../../shared/managed_table';
import { ManagedTable } from '../../../shared/managed_table';
import { TimestampTooltip } from '../../../shared/timestamp_tooltip';
import { isTimeComparison } from '../../../shared/time_comparison/get_comparison_options';
import { ErrorGroupItem, useErrorGroupListData } from './use_error_group_list_data';
@ -84,9 +85,8 @@ export function ErrorGroupList({
const { offset } = query;
const [renderedItems, setRenderedItems] = useState<ErrorGroupItem[]>([]);
const [sorting, setSorting] = useState<TableOptions<ErrorGroupItem>['sort']>(defaultSorting);
const [renderedItemIndices, setRenderedItemIndices] = useState<VisibleItemsStartEnd>([0, 0]);
const {
setDebouncedSearchQuery,
@ -94,7 +94,7 @@ export function ErrorGroupList({
mainStatisticsStatus,
detailedStatistics,
detailedStatisticsStatus,
} = useErrorGroupListData({ renderedItems, sorting });
} = useErrorGroupListData({ renderedItemIndices, sorting });
const isMainStatsLoading = isPending(mainStatisticsStatus);
const isDetailedStatsLoading = isPending(detailedStatisticsStatus);
@ -299,10 +299,10 @@ export function ErrorGroupList({
initialPageSize={initialPageSize}
isLoading={isMainStatsLoading}
tableSearchBar={tableSearchBar}
onChangeRenderedItems={setRenderedItems}
onChangeSorting={setSorting}
saveTableOptionsToUrl={saveTableOptionsToUrl}
showPerPageOptions={showPerPageOptions}
onChangeItemIndices={setRenderedItemIndices}
/>
);
}

View file

@ -5,13 +5,14 @@
* 2.0.
*/
import { useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { useStateDebounced } from '../../../../hooks/use_debounce';
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
import { TableOptions } from '../../../shared/managed_table';
import type { APIReturnType } from '../../../../services/rest/create_call_apm_api';
import type { TableOptions, VisibleItemsStartEnd } from '../../../shared/managed_table';
import { useAnyOfApmParams } from '../../../../hooks/use_apm_params';
import { isTimeComparison } from '../../../shared/time_comparison/get_comparison_options';
@ -34,10 +35,10 @@ const INITIAL_STATE_DETAILED_STATISTICS: DetailedStatistics = {
};
export function useErrorGroupListData({
renderedItems,
renderedItemIndices,
sorting,
}: {
renderedItems: ErrorGroupItem[];
renderedItemIndices: VisibleItemsStartEnd;
sorting: TableOptions<ErrorGroupItem>['sort'];
}) {
const { serviceName } = useApmServiceContext();
@ -80,12 +81,21 @@ export function useErrorGroupListData({
[sorting.direction, sorting.field, start, end, serviceName, environment, kuery, searchQuery]
);
const itemsToFetch = useMemo(
() =>
mainStatistics.errorGroups
.slice(...renderedItemIndices)
.map(({ groupId }) => groupId)
.sort(),
[mainStatistics.errorGroups, renderedItemIndices]
);
const {
data: detailedStatistics = INITIAL_STATE_DETAILED_STATISTICS,
status: detailedStatisticsStatus,
} = useFetcher(
(callApmApi) => {
if (mainStatistics.requestId && renderedItems.length && start && end) {
if (mainStatistics.requestId && itemsToFetch.length && start && end) {
return callApmApi(
'POST /internal/apm/services/{serviceName}/errors/groups/detailed_statistics',
{
@ -100,7 +110,7 @@ export function useErrorGroupListData({
offset: comparisonEnabled && isTimeComparison(offset) ? offset : undefined,
},
body: {
groupIds: JSON.stringify(renderedItems.map(({ groupId }) => groupId).sort()),
groupIds: JSON.stringify(itemsToFetch),
},
},
}
@ -109,7 +119,7 @@ export function useErrorGroupListData({
},
// only fetches agg results when main statistics are ready
// eslint-disable-next-line react-hooks/exhaustive-deps
[mainStatistics.requestId, renderedItems, comparisonEnabled, offset],
[mainStatistics.requestId, itemsToFetch, comparisonEnabled, offset],
{ preservePreviousData: false }
);

View file

@ -21,8 +21,8 @@ import { useLocalStorage } from '../../../hooks/use_local_storage';
import { usePreferredDataSourceAndBucketSize } from '../../../hooks/use_preferred_data_source_and_bucket_size';
import { useProgressiveFetcher } from '../../../hooks/use_progressive_fetcher';
import { useTimeRange } from '../../../hooks/use_time_range';
import { APIReturnType } from '../../../services/rest/create_call_apm_api';
import { SortFunction } from '../../shared/managed_table';
import type { APIReturnType } from '../../../services/rest/create_call_apm_api';
import type { SortFunction, VisibleItemsStartEnd } from '../../shared/managed_table';
import { MLCallout, shouldDisplayMlCallout } from '../../shared/ml_callout';
import { SearchBar } from '../../shared/search_bar/search_bar';
import { isTimeComparison } from '../../shared/time_comparison/get_comparison_options';
@ -113,10 +113,10 @@ function useServicesMainStatisticsFetcher(searchQuery: string | undefined) {
function useServicesDetailedStatisticsFetcher({
mainStatisticsFetch,
renderedItems,
renderedItemIndices,
}: {
mainStatisticsFetch: ReturnType<typeof useServicesMainStatisticsFetcher>;
renderedItems: ServiceListItem[];
renderedItemIndices: VisibleItemsStartEnd;
}) {
const {
query: { rangeFrom, rangeTo, environment, kuery, offset, comparisonEnabled },
@ -134,14 +134,21 @@ function useServicesDetailedStatisticsFetcher({
const { mainStatisticsData, mainStatisticsStatus } = mainStatisticsFetch;
const itemsToFetch = useMemo(
() =>
mainStatisticsData.items
.slice(...renderedItemIndices)
.map(({ serviceName }) => serviceName)
.sort(),
[mainStatisticsData.items, renderedItemIndices]
);
const comparisonFetch = useProgressiveFetcher(
(callApmApi) => {
const serviceNames = renderedItems.map(({ serviceName }) => serviceName);
if (
start &&
end &&
serviceNames.length > 0 &&
itemsToFetch.length > 0 &&
mainStatisticsStatus === FETCH_STATUS.SUCCESS &&
dataSourceOptions
) {
@ -159,7 +166,7 @@ function useServicesDetailedStatisticsFetcher({
},
body: {
// Service name is sorted to guarantee the same order every time this API is called so the result can be cached.
serviceNames: JSON.stringify(serviceNames.sort()),
serviceNames: JSON.stringify(itemsToFetch),
},
},
});
@ -167,7 +174,7 @@ function useServicesDetailedStatisticsFetcher({
},
// only fetches detailed statistics when requestId is invalidated by main statistics api call or offset is changed
// eslint-disable-next-line react-hooks/exhaustive-deps
[mainStatisticsData.requestId, renderedItems, offset, comparisonEnabled],
[mainStatisticsData.requestId, itemsToFetch, offset, comparisonEnabled],
{ preservePreviousData: false }
);
@ -177,8 +184,8 @@ function useServicesDetailedStatisticsFetcher({
export function ServiceInventory() {
const [debouncedSearchQuery, setDebouncedSearchQuery] = useStateDebounced('');
const { onPageReady } = usePerformanceContext();
const [renderedItems, setRenderedItems] = useState<ServiceListItem[]>([]);
const mainStatisticsFetch = useServicesMainStatisticsFetcher(debouncedSearchQuery);
const [renderedItemIndices, setRenderedItemIndices] = useState<VisibleItemsStartEnd>([0, 0]);
const { mainStatisticsData, mainStatisticsStatus } = mainStatisticsFetch;
const displayHealthStatus = mainStatisticsData.items.some((item) => 'healthStatus' in item);
@ -199,7 +206,7 @@ export function ServiceInventory() {
const { comparisonFetch } = useServicesDetailedStatisticsFetcher({
mainStatisticsFetch,
renderedItems,
renderedItemIndices,
});
const { anomalyDetectionSetupState } = useAnomalyDetectionJobsContext();
@ -309,8 +316,8 @@ export function ServiceInventory() {
initialPageSize={INITIAL_PAGE_SIZE}
serviceOverflowCount={serviceOverflowCount}
onChangeSearchQuery={setDebouncedSearchQuery}
onChangeItemIndices={setRenderedItemIndices}
maxCountExceeded={mainStatisticsData?.maxCountExceeded ?? false}
onChangeRenderedItems={setRenderedItems}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -45,12 +45,13 @@ import { ChartType, getTimeSeriesColor } from '../../../shared/charts/helper/get
import { EnvironmentBadge } from '../../../shared/environment_badge';
import { ServiceLink } from '../../../shared/links/apm/service_link';
import { ListMetric } from '../../../shared/list_metric';
import {
import type {
ITableColumn,
ManagedTable,
SortFunction,
TableSearchBar,
VisibleItemsStartEnd,
} from '../../../shared/managed_table';
import { ManagedTable } from '../../../shared/managed_table';
import { ColumnHeaderWithTooltip } from './column_header_with_tooltip';
import { HealthBadge } from './health_badge';
@ -297,8 +298,9 @@ interface Props {
sortFn: SortFunction<ServiceListItem>;
serviceOverflowCount: number;
maxCountExceeded: boolean;
onChangeSearchQuery: (searchQuery: string) => void;
onChangeRenderedItems: (renderedItems: ServiceListItem[]) => void;
onChangeSearchQuery?: (searchQuery: string) => void;
onChangeRenderedItems?: (renderedItems: ServiceListItem[]) => void;
onChangeItemIndices?: (range: VisibleItemsStartEnd) => void;
}
export function ApmServicesTable({
status,
@ -316,6 +318,7 @@ export function ApmServicesTable({
maxCountExceeded,
onChangeSearchQuery,
onChangeRenderedItems,
onChangeItemIndices,
}: Props) {
const breakpoints = useBreakpoints();
const { core } = useApmPluginContext();
@ -426,6 +429,7 @@ export function ApmServicesTable({
initialPageSize={initialPageSize}
sortFn={sortFn}
onChangeRenderedItems={onChangeRenderedItems}
onChangeItemIndices={onChangeItemIndices}
tableSearchBar={tableSearchBar}
/>
</EuiFlexItem>

View file

@ -19,6 +19,12 @@ import {
type SortDirection = 'asc' | 'desc';
/**
* A tuple of the start and end indices for all visible/rendered items
* for the `ManagedTable` component.
*/
export type VisibleItemsStartEnd = readonly [number, number];
export interface TableOptions<T> {
page: { index: number; size: number };
sort: { direction: SortDirection; field: keyof T };
@ -42,7 +48,7 @@ export interface TableSearchBar<T> {
fieldsToSearch: Array<keyof T>;
maxCountExceeded: boolean;
placeholder: string;
onChangeSearchQuery: (searchQuery: string) => void;
onChangeSearchQuery?: (searchQuery: string) => void;
techPreview?: boolean;
}
@ -83,6 +89,7 @@ function UnoptimizedManagedTable<T extends object>(props: {
// onChange handlers
onChangeRenderedItems?: (renderedItems: T[]) => void;
onChangeSorting?: (sorting: TableOptions<T>['sort']) => void;
onChangeItemIndices?: (range: VisibleItemsStartEnd) => void;
// sorting
sortItems?: boolean;
@ -112,8 +119,9 @@ function UnoptimizedManagedTable<T extends object>(props: {
showPerPageOptions = true,
// onChange handlers
onChangeRenderedItems = () => {},
onChangeSorting = () => {},
onChangeRenderedItems,
onChangeSorting,
onChangeItemIndices,
// sorting
sortItems = true,
@ -194,35 +202,43 @@ function UnoptimizedManagedTable<T extends object>(props: {
});
}, [items, searchQuery, tableSearchBar.fieldsToSearch]);
const renderedIndices = useMemo<VisibleItemsStartEnd>(
() => [
tableOptions.page.index * tableOptions.page.size,
(tableOptions.page.index + 1) * tableOptions.page.size,
],
[tableOptions.page.index, tableOptions.page.size]
);
const renderedItems = useMemo(() => {
const sortedItems = sortItems
? sortFn(filteredItems, tableOptions.sort.field as keyof T, tableOptions.sort.direction)
: filteredItems;
return sortedItems.slice(
tableOptions.page.index * tableOptions.page.size,
(tableOptions.page.index + 1) * tableOptions.page.size
);
return sortedItems.slice(...renderedIndices);
}, [
sortItems,
sortFn,
filteredItems,
tableOptions.sort.field,
tableOptions.sort.direction,
tableOptions.page.index,
tableOptions.page.size,
renderedIndices,
]);
useEffect(() => {
onChangeRenderedItems(renderedItems);
onChangeRenderedItems?.(renderedItems);
}, [onChangeRenderedItems, renderedItems]);
useEffect(() => {
onChangeItemIndices?.(renderedIndices);
}, [onChangeItemIndices, renderedIndices]);
const sorting = useMemo(
() => ({ sort: tableOptions.sort as TableOptions<T>['sort'] }),
[tableOptions.sort]
);
useEffect(() => onChangeSorting(sorting.sort), [onChangeSorting, sorting]);
useEffect(() => onChangeSorting?.(sorting.sort), [onChangeSorting, sorting]);
const paginationProps = useMemo(() => {
if (!pagination) {
@ -253,7 +269,7 @@ function UnoptimizedManagedTable<T extends object>(props: {
oldSearchQuery: searchQuery,
})
) {
tableSearchBar.onChangeSearchQuery(value);
tableSearchBar.onChangeSearchQuery?.(value);
}
},
[searchQuery, tableSearchBar]

View file

@ -9,7 +9,6 @@ import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { v4 as uuidv4 } from 'uuid';
import { FormattedMessage } from '@kbn/i18n-react';
import { compact } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { apmEnableTableSearchBar } from '@kbn/observability-plugin/common';
import { ApmDocumentType } from '../../../../common/document_type';
@ -26,7 +25,8 @@ import { FETCH_STATUS, isPending, useFetcher } from '../../../hooks/use_fetcher'
import { usePreferredDataSourceAndBucketSize } from '../../../hooks/use_preferred_data_source_and_bucket_size';
import { APIReturnType } from '../../../services/rest/create_call_apm_api';
import { TransactionOverviewLink } from '../links/apm/transaction_overview_link';
import { ManagedTable, TableSearchBar } from '../managed_table';
import type { TableSearchBar, VisibleItemsStartEnd } from '../managed_table';
import { ManagedTable } from '../managed_table';
import { OverviewTableContainer } from '../overview_table_container';
import { isTimeComparison } from '../time_comparison/get_comparison_options';
import { getColumns } from './get_columns';
@ -73,6 +73,7 @@ export function TransactionsTable({
showSparkPlots,
}: Props) {
const { link } = useApmRouter();
const [renderedItemIndices, setRenderedItemIndices] = useState<VisibleItemsStartEnd>([0, 0]);
const {
query,
@ -91,12 +92,9 @@ export function TransactionsTable({
const { transactionType, serviceName } = useApmServiceContext();
const [searchQuery, setSearchQueryDebounced] = useStateDebounced('');
const [renderedItems, setRenderedItems] = useState<ApiResponse['transactionGroups']>([]);
const { mainStatistics, mainStatisticsStatus, detailedStatistics, detailedStatisticsStatus } =
useTableData({
comparisonEnabled,
currentPageItems: renderedItems,
end,
environment,
kuery,
@ -106,6 +104,7 @@ export function TransactionsTable({
serviceName,
start,
transactionType,
renderedItemIndices,
});
const columns = useMemo(() => {
@ -246,8 +245,8 @@ export function TransactionsTable({
isLoading={mainStatisticsStatus === FETCH_STATUS.LOADING}
tableSearchBar={tableSearchBar}
showPerPageOptions={showPerPageOptions}
onChangeRenderedItems={setRenderedItems}
saveTableOptionsToUrl={saveTableOptionsToUrl}
onChangeItemIndices={setRenderedItemIndices}
/>
</OverviewTableContainer>
</EuiFlexItem>
@ -257,7 +256,6 @@ export function TransactionsTable({
function useTableData({
comparisonEnabled,
currentPageItems,
end,
environment,
kuery,
@ -267,9 +265,9 @@ function useTableData({
serviceName,
start,
transactionType,
renderedItemIndices,
}: {
comparisonEnabled: boolean | undefined;
currentPageItems: ApiResponse['transactionGroups'];
end: string;
environment: string;
kuery: string;
@ -279,6 +277,7 @@ function useTableData({
serviceName: string;
start: string;
transactionType: string | undefined;
renderedItemIndices: VisibleItemsStartEnd;
}) {
const preferredDataSource = usePreferredDataSourceAndBucketSize({
start,
@ -333,16 +332,25 @@ function useTableData({
]
);
const itemsToFetch = useMemo(
() =>
mainStatistics.transactionGroups
.slice(...renderedItemIndices)
.map(({ name }) => name)
.filter((name) => Boolean(name))
.sort(),
[renderedItemIndices, mainStatistics.transactionGroups]
);
const { data: detailedStatistics, status: detailedStatisticsStatus } = useFetcher(
(callApmApi) => {
const transactionNames = compact(currentPageItems.map(({ name }) => name));
if (
start &&
end &&
transactionType &&
latencyAggregationType &&
preferredDataSource &&
transactionNames.length > 0
itemsToFetch.length > 0
) {
return callApmApi(
'GET /internal/apm/services/{serviceName}/transactions/groups/detailed_statistics',
@ -360,7 +368,7 @@ function useTableData({
rollupInterval: preferredDataSource.source.rollupInterval,
useDurationSummary: !!shouldUseDurationSummary,
latencyAggregationType: latencyAggregationType as LatencyAggregationType,
transactionNames: JSON.stringify(transactionNames.sort()),
transactionNames: JSON.stringify(itemsToFetch),
offset: comparisonEnabled && isTimeComparison(offset) ? offset : undefined,
},
},
@ -370,7 +378,7 @@ function useTableData({
},
// only fetches detailed statistics when `currentPageItems` is updated.
// eslint-disable-next-line react-hooks/exhaustive-deps
[mainStatistics.requestId, currentPageItems, offset, comparisonEnabled],
[mainStatistics.requestId, itemsToFetch, offset, comparisonEnabled],
{ preservePreviousData: false }
);