mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Perfomance] Add is_initial_load
meta (#206645)
closes https://github.com/elastic/observability-dev/issues/4185 ## Summary This PR adds the `is_initial_load` parameter to the meta field to distinguish whether the `onPageReady` trigger occurs during the initial load or a page refresh. Refactoring: - Removed the `target` field. as `context.pageName` now provides the necessary information - Refactor APM instrumentation to simplify it Fixes: - https://github.com/elastic/observability-dev/issues/3464 ### ⚠️ Instrumentation The plugins need to call the following function: ``` onPageRefreshStart()``` This method adds a performance marker `start::pageRefresh` to indicate when a page refresh begins. This marker is used along with an end marker `end::pageReady` to determine the total refresh duration. https://github.com/user-attachments/assets/62587d18-b33e-437b-9774-d8e196dbf764 https://github.com/user-attachments/assets/e9c9a761-57bc-4743-9cc7-ea7634696ee3 ### How to test - Checkout the PR - make sure you run `yarn kbn bootstrap` - go to any page that has onPageReady function instrumented (ex services) ### TODO - Once approved, update docs --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
471c413299
commit
9a1d70d5d2
23 changed files with 394 additions and 254 deletions
|
@ -114,6 +114,11 @@ describe('trackPerformanceMeasureEntries', () => {
|
|||
anyKey: 'anyKey',
|
||||
anyValue: 'anyValue',
|
||||
},
|
||||
meta: {
|
||||
isInitialLoad: true,
|
||||
queryRangeSecs: 86400,
|
||||
queryOffsetSecs: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
@ -124,7 +129,7 @@ describe('trackPerformanceMeasureEntries', () => {
|
|||
duration: 1000,
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
key1: 'key1',
|
||||
meta: { target: '/' },
|
||||
meta: { is_initial_load: true, query_range_secs: 86400, query_offset_secs: 0 },
|
||||
value1: 'value1',
|
||||
});
|
||||
});
|
||||
|
@ -152,7 +157,7 @@ describe('trackPerformanceMeasureEntries', () => {
|
|||
expect(analyticsClientMock.reportEvent).toHaveBeenCalledWith('performance_metric', {
|
||||
duration: 1000,
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
meta: { target: '/', query_range_secs: 86400, query_offset_secs: 0 },
|
||||
meta: { query_range_secs: 86400, query_offset_secs: 0 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -72,9 +72,9 @@ export function trackPerformanceMeasureEntries(analytics: AnalyticsClient, isDev
|
|||
duration,
|
||||
...customMetrics,
|
||||
meta: {
|
||||
target,
|
||||
query_range_secs: meta?.queryRangeSecs,
|
||||
query_offset_secs: meta?.queryOffsetSecs,
|
||||
is_initial_load: meta?.isInitialLoad,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
@ -12,24 +12,25 @@ import {
|
|||
getOffsetFromNowInSeconds,
|
||||
getTimeDifferenceInSeconds,
|
||||
} from '@kbn/timerange';
|
||||
import { perfomanceMarkers } from '../../performance_markers';
|
||||
import { EventData } from '../performance_context';
|
||||
import { perfomanceMarkers } from '../../performance_markers';
|
||||
|
||||
interface PerformanceMeta {
|
||||
queryRangeSecs: number;
|
||||
queryOffsetSecs: number;
|
||||
isInitialLoad?: boolean;
|
||||
}
|
||||
|
||||
export function measureInteraction() {
|
||||
export function measureInteraction(pathname: string) {
|
||||
performance.mark(perfomanceMarkers.startPageChange);
|
||||
const trackedRoutes: string[] = [];
|
||||
|
||||
return {
|
||||
/**
|
||||
* Marks the end of the page ready state and measures the performance between the start of the page change and the end of the page ready state.
|
||||
* @param pathname - The pathname of the page.
|
||||
* @param customMetrics - Custom metrics to be included in the performance measure.
|
||||
*/
|
||||
pageReady(pathname: string, eventData?: EventData) {
|
||||
pageReady(eventData?: EventData) {
|
||||
let performanceMeta: PerformanceMeta | undefined;
|
||||
performance.mark(perfomanceMarkers.endPageReady);
|
||||
|
||||
|
@ -49,19 +50,48 @@ export function measureInteraction() {
|
|||
};
|
||||
}
|
||||
|
||||
if (!trackedRoutes.includes(pathname)) {
|
||||
performance.measure(pathname, {
|
||||
if (
|
||||
performance.getEntriesByName(perfomanceMarkers.startPageChange).length > 0 &&
|
||||
performance.getEntriesByName(perfomanceMarkers.endPageReady).length > 0
|
||||
) {
|
||||
performance.measure(`[ttfmp:initial] - ${pathname}`, {
|
||||
detail: {
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
customMetrics: eventData?.customMetrics,
|
||||
meta: performanceMeta,
|
||||
meta: { ...performanceMeta, isInitialLoad: true },
|
||||
},
|
||||
start: perfomanceMarkers.startPageChange,
|
||||
end: perfomanceMarkers.endPageReady,
|
||||
});
|
||||
trackedRoutes.push(pathname);
|
||||
|
||||
// Clean up the marks once the measure is done
|
||||
performance.clearMarks(perfomanceMarkers.startPageChange);
|
||||
performance.clearMarks(perfomanceMarkers.endPageReady);
|
||||
}
|
||||
|
||||
if (
|
||||
performance.getEntriesByName(perfomanceMarkers.startPageRefresh).length > 0 &&
|
||||
performance.getEntriesByName(perfomanceMarkers.endPageReady).length > 0
|
||||
) {
|
||||
performance.measure(`[ttfmp:refresh] - ${pathname}`, {
|
||||
detail: {
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
customMetrics: eventData?.customMetrics,
|
||||
meta: { ...performanceMeta, isInitialLoad: false },
|
||||
},
|
||||
start: perfomanceMarkers.startPageRefresh,
|
||||
end: perfomanceMarkers.endPageReady,
|
||||
});
|
||||
|
||||
// // Clean up the marks once the measure is done
|
||||
performance.clearMarks(perfomanceMarkers.startPageRefresh);
|
||||
performance.clearMarks(perfomanceMarkers.endPageReady);
|
||||
}
|
||||
},
|
||||
pageRefreshStart() {
|
||||
performance.mark(perfomanceMarkers.startPageRefresh);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -15,147 +15,196 @@ describe('measureInteraction', () => {
|
|||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
performance.mark = jest.fn();
|
||||
performance.measure = jest.fn();
|
||||
});
|
||||
describe('Initial load', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
performance.mark = jest.fn();
|
||||
performance.measure = jest.fn();
|
||||
|
||||
it('should mark the start of the page change', () => {
|
||||
measureInteraction();
|
||||
expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.startPageChange);
|
||||
});
|
||||
|
||||
it('should mark the end of the page ready state and measure performance', () => {
|
||||
const interaction = measureInteraction();
|
||||
const pathname = '/test-path';
|
||||
interaction.pageReady(pathname);
|
||||
|
||||
expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.endPageReady);
|
||||
expect(performance.measure).toHaveBeenCalledWith(pathname, {
|
||||
detail: {
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
},
|
||||
start: perfomanceMarkers.startPageChange,
|
||||
end: perfomanceMarkers.endPageReady,
|
||||
performance.getEntriesByName = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce([{ name: 'start::pageChange' }])
|
||||
.mockReturnValueOnce([{ name: 'end::pageReady' }])
|
||||
.mockReturnValueOnce([]);
|
||||
performance.clearMarks = jest.fn();
|
||||
});
|
||||
});
|
||||
|
||||
it('should include custom metrics and meta in the performance measure', () => {
|
||||
const interaction = measureInteraction();
|
||||
const pathname = '/test-path';
|
||||
const eventData = {
|
||||
customMetrics: { key1: 'foo-metric', value1: 100 },
|
||||
meta: { rangeFrom: 'now-15m', rangeTo: 'now' },
|
||||
};
|
||||
it('should mark the start of the page change', () => {
|
||||
const pathname = '/test-path';
|
||||
measureInteraction(pathname);
|
||||
expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.startPageChange);
|
||||
});
|
||||
|
||||
interaction.pageReady(pathname, eventData);
|
||||
it('should mark the end of the page ready state and measure performance', () => {
|
||||
const pathname = '/test-path';
|
||||
const interaction = measureInteraction(pathname);
|
||||
|
||||
expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.endPageReady);
|
||||
expect(performance.measure).toHaveBeenCalledWith(pathname, {
|
||||
detail: {
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
customMetrics: eventData.customMetrics,
|
||||
meta: {
|
||||
queryRangeSecs: 900,
|
||||
queryOffsetSecs: 0,
|
||||
interaction.pageReady();
|
||||
|
||||
expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.endPageReady);
|
||||
expect(performance.measure).toHaveBeenCalledWith(`[ttfmp:initial] - ${pathname}`, {
|
||||
detail: {
|
||||
customMetrics: undefined,
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
meta: {
|
||||
isInitialLoad: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
end: 'end::pageReady',
|
||||
start: 'start::pageChange',
|
||||
start: perfomanceMarkers.startPageChange,
|
||||
end: perfomanceMarkers.endPageReady,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle absolute date format correctly', () => {
|
||||
const interaction = measureInteraction();
|
||||
const pathname = '/test-path';
|
||||
jest.spyOn(global.Date, 'now').mockReturnValue(1733704200000); // 2024-12-09T00:30:00Z
|
||||
it('should include custom metrics and meta in the performance measure', () => {
|
||||
const pathname = '/test-path';
|
||||
const interaction = measureInteraction(pathname);
|
||||
const eventData = {
|
||||
customMetrics: { key1: 'foo-metric', value1: 100 },
|
||||
meta: { rangeFrom: 'now-15m', rangeTo: 'now' },
|
||||
};
|
||||
|
||||
const eventData = {
|
||||
meta: { rangeFrom: '2024-12-09T00:00:00Z', rangeTo: '2024-12-09T00:30:00Z' },
|
||||
};
|
||||
interaction.pageReady(eventData);
|
||||
|
||||
interaction.pageReady(pathname, eventData);
|
||||
|
||||
expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.endPageReady);
|
||||
expect(performance.measure).toHaveBeenCalledWith(pathname, {
|
||||
detail: {
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
customMetrics: undefined,
|
||||
meta: {
|
||||
queryRangeSecs: 1800,
|
||||
queryOffsetSecs: 0,
|
||||
expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.endPageReady);
|
||||
expect(performance.measure).toHaveBeenCalledWith(`[ttfmp:initial] - ${pathname}`, {
|
||||
detail: {
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
customMetrics: eventData.customMetrics,
|
||||
meta: {
|
||||
queryRangeSecs: 900,
|
||||
queryOffsetSecs: 0,
|
||||
isInitialLoad: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
end: 'end::pageReady',
|
||||
start: 'start::pageChange',
|
||||
end: 'end::pageReady',
|
||||
start: 'start::pageChange',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle negative offset when rangeTo is in the past', () => {
|
||||
const interaction = measureInteraction();
|
||||
const pathname = '/test-path';
|
||||
jest.spyOn(global.Date, 'now').mockReturnValue(1733704200000); // 2024-12-09T00:30:00Z
|
||||
it('should handle absolute date format correctly', () => {
|
||||
const pathname = '/test-path';
|
||||
const interaction = measureInteraction(pathname);
|
||||
jest.spyOn(global.Date, 'now').mockReturnValue(1733704200000); // 2024-12-09T00:30:00Z
|
||||
|
||||
const eventData = {
|
||||
meta: { rangeFrom: '2024-12-08T00:00:00Z', rangeTo: '2024-12-09T00:00:00Z' },
|
||||
};
|
||||
const eventData = {
|
||||
meta: { rangeFrom: '2024-12-09T00:00:00Z', rangeTo: '2024-12-09T00:30:00Z' },
|
||||
};
|
||||
|
||||
interaction.pageReady(pathname, eventData);
|
||||
interaction.pageReady(eventData);
|
||||
|
||||
expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.endPageReady);
|
||||
expect(performance.measure).toHaveBeenCalledWith(pathname, {
|
||||
detail: {
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
customMetrics: undefined,
|
||||
meta: {
|
||||
queryRangeSecs: 86400,
|
||||
queryOffsetSecs: -1800,
|
||||
expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.endPageReady);
|
||||
expect(performance.measure).toHaveBeenCalledWith(`[ttfmp:initial] - ${pathname}`, {
|
||||
detail: {
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
customMetrics: undefined,
|
||||
meta: {
|
||||
queryRangeSecs: 1800,
|
||||
queryOffsetSecs: 0,
|
||||
isInitialLoad: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
end: 'end::pageReady',
|
||||
start: 'start::pageChange',
|
||||
end: 'end::pageReady',
|
||||
start: 'start::pageChange',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle positive offset when rangeTo is in the future', () => {
|
||||
const interaction = measureInteraction();
|
||||
const pathname = '/test-path';
|
||||
jest.spyOn(global.Date, 'now').mockReturnValue(1733704200000); // 2024-12-09T00:30:00Z
|
||||
it('should handle negative offset when rangeTo is in the past', () => {
|
||||
const pathname = '/test-path';
|
||||
const interaction = measureInteraction(pathname);
|
||||
jest.spyOn(global.Date, 'now').mockReturnValue(1733704200000); // 2024-12-09T00:30:00Z
|
||||
|
||||
const eventData = {
|
||||
meta: { rangeFrom: '2024-12-08T01:00:00Z', rangeTo: '2024-12-09T01:00:00Z' },
|
||||
};
|
||||
const eventData = {
|
||||
meta: { rangeFrom: '2024-12-08T00:00:00Z', rangeTo: '2024-12-09T00:00:00Z' },
|
||||
};
|
||||
|
||||
interaction.pageReady(pathname, eventData);
|
||||
interaction.pageReady(eventData);
|
||||
|
||||
expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.endPageReady);
|
||||
expect(performance.measure).toHaveBeenCalledWith(pathname, {
|
||||
detail: {
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
customMetrics: undefined,
|
||||
meta: {
|
||||
queryRangeSecs: 86400,
|
||||
queryOffsetSecs: 1800,
|
||||
expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.endPageReady);
|
||||
expect(performance.measure).toHaveBeenCalledWith(`[ttfmp:initial] - ${pathname}`, {
|
||||
detail: {
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
customMetrics: undefined,
|
||||
meta: {
|
||||
queryRangeSecs: 86400,
|
||||
queryOffsetSecs: -1800,
|
||||
isInitialLoad: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
end: 'end::pageReady',
|
||||
start: 'start::pageChange',
|
||||
end: 'end::pageReady',
|
||||
start: 'start::pageChange',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle positive offset when rangeTo is in the future', () => {
|
||||
const pathname = '/test-path';
|
||||
|
||||
const interaction = measureInteraction(pathname);
|
||||
jest.spyOn(global.Date, 'now').mockReturnValue(1733704200000); // 2024-12-09T00:30:00Z
|
||||
|
||||
const eventData = {
|
||||
meta: { rangeFrom: '2024-12-08T01:00:00Z', rangeTo: '2024-12-09T01:00:00Z' },
|
||||
};
|
||||
|
||||
interaction.pageReady(eventData);
|
||||
|
||||
expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.endPageReady);
|
||||
expect(performance.measure).toHaveBeenCalledWith(`[ttfmp:initial] - ${pathname}`, {
|
||||
detail: {
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
customMetrics: undefined,
|
||||
meta: {
|
||||
queryRangeSecs: 86400,
|
||||
queryOffsetSecs: 1800,
|
||||
isInitialLoad: true,
|
||||
},
|
||||
},
|
||||
end: 'end::pageReady',
|
||||
start: 'start::pageChange',
|
||||
});
|
||||
expect(performance.clearMarks).toHaveBeenCalledWith(perfomanceMarkers.startPageChange);
|
||||
expect(performance.clearMarks).toHaveBeenCalledWith(perfomanceMarkers.endPageReady);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not measure the same route twice', () => {
|
||||
const interaction = measureInteraction();
|
||||
const pathname = '/test-path';
|
||||
describe('Refresh', () => {
|
||||
beforeEach(() => {
|
||||
performance.getEntriesByName = jest
|
||||
.fn()
|
||||
.mockReturnValue([{ name: 'start::pageRefresh' }])
|
||||
.mockReturnValue([{ name: 'end::pageReady' }]);
|
||||
});
|
||||
it('should set isInitialLoad to false on refresh calls', () => {
|
||||
const pathname = '/test-path';
|
||||
const interaction = measureInteraction(pathname);
|
||||
|
||||
interaction.pageReady(pathname);
|
||||
interaction.pageReady(pathname);
|
||||
jest.spyOn(global.Date, 'now').mockReturnValue(1733704200000); // 2024-12-09T00:30:00Z
|
||||
|
||||
expect(performance.measure).toHaveBeenCalledTimes(1);
|
||||
const eventData = {
|
||||
meta: { rangeFrom: '2024-12-08T01:00:00Z', rangeTo: '2024-12-09T01:00:00Z' },
|
||||
};
|
||||
|
||||
interaction.pageReady(eventData);
|
||||
|
||||
expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.endPageReady);
|
||||
expect(performance.measure).toHaveBeenCalledWith(`[ttfmp:refresh] - ${pathname}`, {
|
||||
detail: {
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
customMetrics: undefined,
|
||||
meta: {
|
||||
queryRangeSecs: 86400,
|
||||
queryOffsetSecs: 1800,
|
||||
isInitialLoad: false,
|
||||
},
|
||||
},
|
||||
end: 'end::pageReady',
|
||||
start: 'start::pageRefresh',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,7 +28,8 @@ export interface EventData {
|
|||
export function PerformanceContextProvider({ children }: { children: React.ReactElement }) {
|
||||
const [isRendered, setIsRendered] = useState(false);
|
||||
const location = useLocation();
|
||||
const interaction = measureInteraction();
|
||||
|
||||
const interaction = useMemo(() => measureInteraction(location.pathname), [location.pathname]);
|
||||
|
||||
React.useEffect(() => {
|
||||
afterFrame(() => {
|
||||
|
@ -44,11 +45,14 @@ export function PerformanceContextProvider({ children }: { children: React.React
|
|||
() => ({
|
||||
onPageReady(eventData) {
|
||||
if (isRendered) {
|
||||
interaction.pageReady(location.pathname, eventData);
|
||||
interaction.pageReady(eventData);
|
||||
}
|
||||
},
|
||||
onPageRefreshStart() {
|
||||
interaction.pageRefreshStart();
|
||||
},
|
||||
}),
|
||||
[isRendered, location.pathname, interaction]
|
||||
[isRendered, interaction]
|
||||
);
|
||||
|
||||
return <PerformanceContext.Provider value={api}>{children}</PerformanceContext.Provider>;
|
||||
|
|
|
@ -15,6 +15,19 @@ export interface PerformanceApi {
|
|||
* @param eventData - Data to send with the performance measure, conforming the structure of a {@link EventData}.
|
||||
*/
|
||||
onPageReady(eventData?: EventData): void;
|
||||
/**
|
||||
* Marks the start of a page refresh event for performance tracking.
|
||||
* This method adds a performance marker start::pageRefresh to indicate when a page refresh begins.
|
||||
*
|
||||
* Usage:
|
||||
* ```ts
|
||||
* onPageRefreshStart();
|
||||
* ```
|
||||
*
|
||||
* The marker set by this function can later be used in performance measurements
|
||||
* along with an end marker end::pageReady to determine the total refresh duration.
|
||||
*/
|
||||
onPageRefreshStart(): void;
|
||||
}
|
||||
|
||||
export const PerformanceContext = createContext<PerformanceApi | undefined>(undefined);
|
||||
|
|
|
@ -11,4 +11,5 @@
|
|||
export const perfomanceMarkers = {
|
||||
startPageChange: 'start::pageChange',
|
||||
endPageReady: 'end::pageReady',
|
||||
startPageRefresh: 'start::pageRefresh',
|
||||
};
|
||||
|
|
|
@ -8,10 +8,10 @@
|
|||
import { EuiBadge, EuiIconTip, EuiToolTip, RIGHT_ALIGNMENT } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import styled from '@emotion/styled';
|
||||
import React, { useEffect, useMemo, useState, useRef } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { apmEnableTableSearchBar } from '@kbn/observability-plugin/common';
|
||||
import { usePerformanceContext } from '@kbn/ebt-tools';
|
||||
import { FETCH_STATUS, isPending } from '../../../../hooks/use_fetcher';
|
||||
import { isPending, isSuccess } from '../../../../hooks/use_fetcher';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
||||
import { asBigNumber } from '../../../../../common/utils/formatters';
|
||||
import { useAnyOfApmParams } from '../../../../hooks/use_apm_params';
|
||||
|
@ -85,7 +85,6 @@ export function ErrorGroupList({
|
|||
const { offset, rangeFrom, rangeTo } = query;
|
||||
|
||||
const [renderedItems, setRenderedItems] = useState<ErrorGroupItem[]>([]);
|
||||
const hasTableLoaded = useRef(false);
|
||||
const [sorting, setSorting] = useState<TableOptions<ErrorGroupItem>['sort']>(defaultSorting);
|
||||
|
||||
const {
|
||||
|
@ -103,11 +102,7 @@ export function ErrorGroupList({
|
|||
useEffect(() => {
|
||||
// this component is used both for the service overview tab and the errors tab,
|
||||
// onLoadTable will be defined if it's the service overview tab
|
||||
if (
|
||||
mainStatisticsStatus === FETCH_STATUS.SUCCESS &&
|
||||
detailedStatisticsStatus === FETCH_STATUS.SUCCESS &&
|
||||
!hasTableLoaded.current
|
||||
) {
|
||||
if (isSuccess(mainStatisticsStatus) && isSuccess(detailedStatisticsStatus)) {
|
||||
if (onLoadTable) {
|
||||
onLoadTable();
|
||||
} else {
|
||||
|
@ -118,15 +113,14 @@ export function ErrorGroupList({
|
|||
},
|
||||
});
|
||||
}
|
||||
hasTableLoaded.current = true;
|
||||
}
|
||||
}, [
|
||||
mainStatisticsStatus,
|
||||
detailedStatisticsStatus,
|
||||
onLoadTable,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
onPageReady,
|
||||
onLoadTable,
|
||||
]);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import type { EuiFlexGroupProps } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { usePerformanceContext } from '@kbn/ebt-tools';
|
||||
import { chartHeight } from '..';
|
||||
import type { AgentName } from '../../../../../typings/es_schemas/ui/fields/agent';
|
||||
|
@ -36,6 +36,12 @@ import { useLocalStorage } from '../../../../hooks/use_local_storage';
|
|||
|
||||
const latencyChartHeight = 200;
|
||||
|
||||
export interface TablesLoadedState {
|
||||
transactions: boolean;
|
||||
dependencies: boolean;
|
||||
errors: boolean;
|
||||
}
|
||||
|
||||
export function ApmOverview() {
|
||||
const router = useApmRouter();
|
||||
const { serviceName, fallbackToTransactions, agentName, serverlessType } = useApmServiceContext();
|
||||
|
@ -45,7 +51,7 @@ export function ApmOverview() {
|
|||
} = useApmParams('/services/{serviceName}/overview');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
const [haveTablesLoaded, setHaveTablesLoaded] = useState({
|
||||
const [haveTablesLoaded, setHaveTablesLoaded] = useState<TablesLoadedState>({
|
||||
transactions: false,
|
||||
dependencies: false,
|
||||
errors: false,
|
||||
|
@ -81,9 +87,12 @@ export function ApmOverview() {
|
|||
false
|
||||
);
|
||||
|
||||
const onLoadTable = (key: string) => {
|
||||
const handleOnLoadTable = (key: keyof TablesLoadedState) =>
|
||||
setHaveTablesLoaded((currentValues) => ({ ...currentValues, [key]: true }));
|
||||
};
|
||||
|
||||
const onTransactionsTableLoad = useCallback(() => handleOnLoadTable('transactions'), []);
|
||||
const onErrorsTableLoad = useCallback(() => handleOnLoadTable('errors'), []);
|
||||
const onDependenciesTableLoad = useCallback(() => handleOnLoadTable('dependencies'), []);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -121,7 +130,7 @@ export function ApmOverview() {
|
|||
kuery={kuery}
|
||||
environment={environment}
|
||||
fixedHeight={true}
|
||||
onLoadTable={() => onLoadTable('transactions')}
|
||||
onLoadTable={onTransactionsTableLoad}
|
||||
start={start}
|
||||
end={end}
|
||||
showPerPageOptions={false}
|
||||
|
@ -147,7 +156,7 @@ export function ApmOverview() {
|
|||
<EuiPanel hasBorder={true}>
|
||||
<ServiceOverviewErrorsTable
|
||||
serviceName={serviceName}
|
||||
onLoadTable={() => onLoadTable('errors')}
|
||||
onLoadTable={onErrorsTableLoad}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
|
@ -178,7 +187,7 @@ export function ApmOverview() {
|
|||
<EuiFlexItem grow={7}>
|
||||
<EuiPanel hasBorder={true}>
|
||||
<ServiceOverviewDependenciesTable
|
||||
onLoadTable={() => onLoadTable('dependencies')}
|
||||
onLoadTable={onDependenciesTableLoad}
|
||||
fixedHeight={true}
|
||||
showPerPageOptions={false}
|
||||
link={
|
||||
|
|
|
@ -9,25 +9,26 @@ import { EuiIconTip } from '@elastic/eui';
|
|||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { FETCH_STATUS, useUiTracker } from '@kbn/observability-shared-plugin/public';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useUiTracker } from '@kbn/observability-shared-plugin/public';
|
||||
import { usePerformanceContext } from '@kbn/ebt-tools';
|
||||
import { isTimeComparison } from '../../../shared/time_comparison/get_comparison_options';
|
||||
import { getNodeName, NodeType } from '../../../../../common/connections';
|
||||
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { useFetcher, isSuccess } from '../../../../hooks/use_fetcher';
|
||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||
import { DependencyLink } from '../../../shared/links/dependency_link';
|
||||
import { DependenciesTable } from '../../../shared/dependencies_table';
|
||||
import { ServiceLink } from '../../../shared/links/apm/service_link';
|
||||
import type { TablesLoadedState } from '../apm_overview';
|
||||
|
||||
interface ServiceOverviewDependenciesTableProps {
|
||||
fixedHeight?: boolean;
|
||||
link?: ReactNode;
|
||||
showPerPageOptions?: boolean;
|
||||
showSparkPlots?: boolean;
|
||||
onLoadTable?: () => void;
|
||||
onLoadTable?: (key: keyof TablesLoadedState) => void;
|
||||
}
|
||||
|
||||
export function ServiceOverviewDependenciesTable({
|
||||
|
@ -55,7 +56,6 @@ export function ServiceOverviewDependenciesTable({
|
|||
const { serviceName, transactionType } = useApmServiceContext();
|
||||
const { onPageReady } = usePerformanceContext();
|
||||
const trackEvent = useUiTracker();
|
||||
const hasTableLoaded = useRef(false);
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (!start || !end) {
|
||||
|
@ -79,11 +79,11 @@ export function ServiceOverviewDependenciesTable({
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
// this component is used both for the service overview tab and the transactions tab,
|
||||
// this component is used both for the service overview tab and the dependency tab,
|
||||
// onLoadTable will be defined if it's the service overview tab
|
||||
if (status === FETCH_STATUS.SUCCESS && !hasTableLoaded.current) {
|
||||
if (isSuccess(status)) {
|
||||
if (onLoadTable) {
|
||||
onLoadTable();
|
||||
onLoadTable('dependencies');
|
||||
} else {
|
||||
onPageReady({
|
||||
meta: {
|
||||
|
@ -92,9 +92,8 @@ export function ServiceOverviewDependenciesTable({
|
|||
},
|
||||
});
|
||||
}
|
||||
hasTableLoaded.current = true;
|
||||
}
|
||||
}, [status, onLoadTable, onPageReady, rangeFrom, rangeTo]);
|
||||
}, [status, onPageReady, rangeFrom, rangeTo, onLoadTable]);
|
||||
|
||||
const dependencies =
|
||||
data?.serviceDependencies.map((dependency) => {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { usePerformanceContext } from '@kbn/ebt-tools';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
|
@ -17,39 +17,35 @@ export function TraceExplorerAggregatedCriticalPath() {
|
|||
} = useApmParams('/traces/explorer/critical_path');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
const [hasLoadedTable, setHasLoadedTable] = useState(false);
|
||||
const { onPageReady } = usePerformanceContext();
|
||||
const {
|
||||
data: { traceSamples },
|
||||
status: samplesFetchStatus,
|
||||
} = useTraceExplorerSamples();
|
||||
const { onPageReady } = usePerformanceContext();
|
||||
|
||||
const traceIds = useMemo(() => {
|
||||
return traceSamples.map((sample) => sample.traceId);
|
||||
}, [traceSamples]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasLoadedTable) {
|
||||
onPageReady({
|
||||
meta: {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
},
|
||||
customMetrics: {
|
||||
key1: 'traceIds',
|
||||
value1: traceIds.length,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [hasLoadedTable, onPageReady, rangeFrom, rangeTo, traceIds]);
|
||||
|
||||
const handleOnLoadTable = useCallback(() => {
|
||||
onPageReady({
|
||||
meta: {
|
||||
rangeFrom: start,
|
||||
rangeTo: end,
|
||||
},
|
||||
customMetrics: {
|
||||
key1: 'traceIds',
|
||||
value1: traceIds.length,
|
||||
},
|
||||
});
|
||||
}, [start, end, traceIds, onPageReady]);
|
||||
return (
|
||||
<CriticalPathFlamegraph
|
||||
onLoadTable={() => setHasLoadedTable(true)}
|
||||
start={start}
|
||||
end={end}
|
||||
traceIds={traceIds}
|
||||
traceIdsFetchStatus={samplesFetchStatus}
|
||||
onLoadTable={handleOnLoadTable}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { usePerformanceContext } from '@kbn/ebt-tools';
|
||||
import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { CriticalPathFlamegraph } from '../../shared/critical_path_flamegraph';
|
||||
|
@ -23,12 +24,27 @@ function TransactionDetailAggregatedCriticalPath({ traceSamplesFetchResult }: Ta
|
|||
'/mobile-services/{serviceName}/transactions/view'
|
||||
);
|
||||
|
||||
const { onPageReady } = usePerformanceContext();
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const traceIds = useMemo(() => {
|
||||
return traceSamplesFetchResult.data?.traceSamples.map((sample) => sample.traceId) ?? [];
|
||||
}, [traceSamplesFetchResult.data]);
|
||||
|
||||
const handleOnLoadTable = useCallback(() => {
|
||||
onPageReady({
|
||||
meta: {
|
||||
rangeFrom: start,
|
||||
rangeTo: end,
|
||||
},
|
||||
customMetrics: {
|
||||
key1: 'traceIds',
|
||||
value1: traceIds.length,
|
||||
},
|
||||
});
|
||||
}, [start, end, traceIds, onPageReady]);
|
||||
|
||||
return (
|
||||
<CriticalPathFlamegraph
|
||||
start={start}
|
||||
|
@ -37,6 +53,7 @@ function TransactionDetailAggregatedCriticalPath({ traceSamplesFetchResult }: Ta
|
|||
traceIds={traceIds}
|
||||
serviceName={serviceName}
|
||||
transactionName={transactionName}
|
||||
onLoadTable={handleOnLoadTable}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { usePerformanceContext } from '@kbn/ebt-tools';
|
||||
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { isServerlessAgentName } from '../../../../common/agent_name';
|
||||
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||
|
@ -37,21 +38,9 @@ export function TransactionOverview() {
|
|||
} = useApmParams('/services/{serviceName}/transactions');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
const [hasLoadedTable, setHasLoadedTable] = useState(false);
|
||||
const { onPageReady } = usePerformanceContext();
|
||||
const { transactionType, fallbackToTransactions, serverlessType, serviceName } =
|
||||
useApmServiceContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (hasLoadedTable) {
|
||||
onPageReady({
|
||||
meta: {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [hasLoadedTable, onPageReady, rangeFrom, rangeTo]);
|
||||
const { onPageReady } = usePerformanceContext();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
|
@ -80,6 +69,15 @@ export function TransactionOverview() {
|
|||
const hasLogsOnlySignal =
|
||||
serviceEntitySummary?.dataStreamTypes && isLogsOnlySignal(serviceEntitySummary.dataStreamTypes);
|
||||
|
||||
const handleOnLoadTable = useCallback(() => {
|
||||
onPageReady({
|
||||
meta: {
|
||||
rangeFrom: start,
|
||||
rangeTo: end,
|
||||
},
|
||||
});
|
||||
}, [start, end, onPageReady]);
|
||||
|
||||
if (hasLogsOnlySignal) {
|
||||
return <ServiceTabEmptyState id="transactionOverview" />;
|
||||
}
|
||||
|
@ -122,12 +120,12 @@ export function TransactionOverview() {
|
|||
hideViewTransactionsLink
|
||||
numberOfTransactionsPerPage={10}
|
||||
showMaxTransactionGroupsExceededWarning
|
||||
onLoadTable={() => setHasLoadedTable(true)}
|
||||
environment={environment}
|
||||
kuery={kuery}
|
||||
start={start}
|
||||
end={end}
|
||||
saveTableOptionsToUrl
|
||||
onLoadTable={handleOnLoadTable}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
|
|
|
@ -10,9 +10,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, euiPaletteColorBlind } fr
|
|||
import { css } from '@emotion/css';
|
||||
import { useChartThemes } from '@kbn/observability-shared-plugin/public';
|
||||
import { uniqueId } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
import type { FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
import { isSuccess } from '../../../hooks/use_fetcher';
|
||||
import { useFetcher, isPending } from '../../../hooks/use_fetcher';
|
||||
import { CriticalPathFlamegraphTooltip } from './critical_path_flamegraph_tooltip';
|
||||
import { criticalPathToFlamegraph } from './critical_path_to_flamegraph';
|
||||
|
@ -41,7 +42,6 @@ export function CriticalPathFlamegraph(
|
|||
// of the search.
|
||||
const timerange = useRef({ start, end });
|
||||
timerange.current = { start, end };
|
||||
const [hasTableLoaded, setHasTableLoaded] = useState(false);
|
||||
|
||||
const { data: { criticalPath } = { criticalPath: null }, status: criticalPathFetchStatus } =
|
||||
useFetcher(
|
||||
|
@ -66,22 +66,10 @@ export function CriticalPathFlamegraph(
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
criticalPathFetchStatus === FETCH_STATUS.SUCCESS &&
|
||||
traceIdsFetchStatus === FETCH_STATUS.SUCCESS &&
|
||||
onLoadTable &&
|
||||
!hasTableLoaded
|
||||
) {
|
||||
onLoadTable();
|
||||
setHasTableLoaded(true);
|
||||
if (isSuccess(criticalPathFetchStatus) && isSuccess(traceIdsFetchStatus)) {
|
||||
onLoadTable?.();
|
||||
}
|
||||
}, [
|
||||
criticalPathFetchStatus,
|
||||
onLoadTable,
|
||||
hasTableLoaded,
|
||||
traceIdsFetchStatus,
|
||||
setHasTableLoaded,
|
||||
]);
|
||||
}, [start, end, criticalPathFetchStatus, traceIdsFetchStatus, traceIds, onLoadTable]);
|
||||
|
||||
const chartThemes = useChartThemes();
|
||||
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getByTestId, fireEvent, getByText, act } from '@testing-library/react';
|
||||
import { getByTestId, fireEvent, getByText, act, waitFor } from '@testing-library/react';
|
||||
import type { MemoryHistory } from 'history';
|
||||
import { PerformanceContextProvider } from '@kbn/ebt-tools';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import React from 'react';
|
||||
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
|
@ -79,7 +80,9 @@ function setup({
|
|||
<UrlParamsProvider>
|
||||
<ApmTimeRangeMetadataContextProvider>
|
||||
<ApmServiceContextProvider>
|
||||
<SearchBar showTransactionTypeSelector />
|
||||
<PerformanceContextProvider>
|
||||
<SearchBar showTransactionTypeSelector />
|
||||
</PerformanceContextProvider>
|
||||
</ApmServiceContextProvider>
|
||||
</ApmTimeRangeMetadataContextProvider>
|
||||
</UrlParamsProvider>
|
||||
|
@ -96,7 +99,7 @@ describe('when transactionType is selected and multiple transaction types are gi
|
|||
jest.spyOn(history, 'replace');
|
||||
});
|
||||
|
||||
it('renders a radio group with transaction types', () => {
|
||||
it('renders a radio group with transaction types', async () => {
|
||||
const { container } = setup({
|
||||
history,
|
||||
serviceTransactionTypes: ['firstType', 'secondType'],
|
||||
|
@ -108,14 +111,16 @@ describe('when transactionType is selected and multiple transaction types are gi
|
|||
});
|
||||
|
||||
// transaction type selector
|
||||
const dropdown = getByTestId(container, 'headerFilterTransactionType');
|
||||
await waitFor(() => {
|
||||
const dropdown = getByTestId(container, 'headerFilterTransactionType');
|
||||
|
||||
// both options should be listed
|
||||
expect(getByText(dropdown, 'firstType')).toBeInTheDocument();
|
||||
expect(getByText(dropdown, 'secondType')).toBeInTheDocument();
|
||||
// both options should be listed
|
||||
expect(getByText(dropdown, 'firstType')).toBeInTheDocument();
|
||||
expect(getByText(dropdown, 'secondType')).toBeInTheDocument();
|
||||
|
||||
// second option should be selected
|
||||
expect(dropdown).toHaveValue('secondType');
|
||||
// second option should be selected
|
||||
expect(dropdown).toHaveValue('secondType');
|
||||
});
|
||||
});
|
||||
|
||||
it('should update the URL when a transaction type is selected', async () => {
|
||||
|
|
|
@ -20,7 +20,7 @@ import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
|
|||
import { useApmRouter } from '../../../hooks/use_apm_router';
|
||||
import { useBreakpoints } from '../../../hooks/use_breakpoints';
|
||||
import { useStateDebounced } from '../../../hooks/use_debounce';
|
||||
import { FETCH_STATUS, isPending, useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { FETCH_STATUS, isPending, isSuccess, useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { usePreferredDataSourceAndBucketSize } from '../../../hooks/use_preferred_data_source_and_bucket_size';
|
||||
import type { APIReturnType } from '../../../services/rest/create_call_apm_api';
|
||||
import { TransactionOverviewLink } from '../links/apm/transaction_overview_link';
|
||||
|
@ -91,7 +91,6 @@ export function TransactionsTable({
|
|||
const shouldShowSparkPlots = showSparkPlots ?? !isLarge;
|
||||
const { transactionType, serviceName } = useApmServiceContext();
|
||||
const [searchQuery, setSearchQueryDebounced] = useStateDebounced('');
|
||||
const [hasTableLoaded, setHasTableLoaded] = useState(false);
|
||||
const [renderedItems, setRenderedItems] = useState<ApiResponse['transactionGroups']>([]);
|
||||
|
||||
const { mainStatistics, mainStatisticsStatus, detailedStatistics, detailedStatisticsStatus } =
|
||||
|
@ -110,16 +109,10 @@ export function TransactionsTable({
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
mainStatisticsStatus === FETCH_STATUS.SUCCESS &&
|
||||
detailedStatisticsStatus === FETCH_STATUS.SUCCESS &&
|
||||
onLoadTable &&
|
||||
!hasTableLoaded
|
||||
) {
|
||||
onLoadTable();
|
||||
setHasTableLoaded(true);
|
||||
if (isSuccess(mainStatisticsStatus) && isSuccess(detailedStatisticsStatus)) {
|
||||
onLoadTable?.();
|
||||
}
|
||||
}, [mainStatisticsStatus, detailedStatisticsStatus, onLoadTable, hasTableLoaded]);
|
||||
}, [mainStatisticsStatus, detailedStatisticsStatus, onLoadTable, end, start]);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return getColumns({
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
toElasticsearchQuery,
|
||||
} from '@kbn/es-query';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { usePerformanceContext } from '@kbn/ebt-tools';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import qs from 'query-string';
|
||||
|
@ -141,6 +142,7 @@ export function UnifiedSearchBar({
|
|||
|
||||
const { kuery, serviceName, environment, groupId, refreshPausedFromUrl, refreshIntervalFromUrl } =
|
||||
useSearchBarParams(value);
|
||||
const { onPageRefreshStart } = usePerformanceContext();
|
||||
const timePickerTimeDefaults = core.uiSettings.get<TimePickerTimeDefaults>(
|
||||
UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS
|
||||
);
|
||||
|
@ -204,6 +206,7 @@ export function UnifiedSearchBar({
|
|||
const onRefresh = () => {
|
||||
clearCache();
|
||||
incrementTimeRangeId();
|
||||
onPageRefreshStart();
|
||||
};
|
||||
|
||||
const onRefreshChange = ({ isPaused, refreshInterval }: Partial<OnRefreshChangeProps>) => {
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
|
||||
import { mount } from 'enzyme';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { PerformanceContextProvider } from '@kbn/ebt-tools';
|
||||
import type { MemoryHistory } from 'history';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import React from 'react';
|
||||
|
@ -25,7 +27,7 @@ jest.mock('react-router-dom', () => ({
|
|||
useLocation: jest.fn(),
|
||||
}));
|
||||
|
||||
function setup({ urlParams, history }: { urlParams: UrlParams; history: MemoryHistory }) {
|
||||
async function setup({ urlParams, history }: { urlParams: UrlParams; history: MemoryHistory }) {
|
||||
history.replace({
|
||||
pathname: '/services',
|
||||
search: fromQuery(urlParams),
|
||||
|
@ -73,7 +75,9 @@ function setup({ urlParams, history }: { urlParams: UrlParams; history: MemoryHi
|
|||
} as unknown as ApmPluginContextValue
|
||||
}
|
||||
>
|
||||
<UnifiedSearchBar />
|
||||
<PerformanceContextProvider>
|
||||
<UnifiedSearchBar />
|
||||
</PerformanceContextProvider>
|
||||
</MockApmPluginContextWrapper>
|
||||
);
|
||||
|
||||
|
@ -89,6 +93,7 @@ function setup({ urlParams, history }: { urlParams: UrlParams; history: MemoryHi
|
|||
|
||||
describe('when kuery is already present in the url, the search bar must reflect the same', () => {
|
||||
let history: MemoryHistory;
|
||||
|
||||
beforeEach(() => {
|
||||
history = createMemoryHistory();
|
||||
jest.spyOn(history, 'push');
|
||||
|
@ -103,12 +108,12 @@ describe('when kuery is already present in the url, the search bar must reflect
|
|||
|
||||
const search = '?method=json';
|
||||
const pathname = '/services';
|
||||
(useLocation as jest.Mock).mockImplementationOnce(() => ({
|
||||
(useLocation as jest.Mock).mockReturnValue(() => ({
|
||||
search,
|
||||
pathname,
|
||||
}));
|
||||
|
||||
it('sets the searchbar value based on URL', () => {
|
||||
it('sets the searchbar value based on URL', async () => {
|
||||
const expectedQuery = {
|
||||
query: 'service.name:"opbeans-android"',
|
||||
language: 'kuery',
|
||||
|
@ -137,13 +142,15 @@ describe('when kuery is already present in the url, the search bar must reflect
|
|||
};
|
||||
jest.spyOn(useApmParamsHook, 'useApmParams').mockReturnValue({ query: urlParams, path: {} });
|
||||
|
||||
const { setQuerySpy, setTimeSpy, setRefreshIntervalSpy } = setup({
|
||||
const { setQuerySpy, setTimeSpy, setRefreshIntervalSpy } = await setup({
|
||||
history,
|
||||
urlParams,
|
||||
});
|
||||
|
||||
expect(setQuerySpy).toBeCalledWith(expectedQuery);
|
||||
expect(setTimeSpy).toBeCalledWith(expectedTimeRange);
|
||||
expect(setRefreshIntervalSpy).toBeCalledWith(refreshInterval);
|
||||
await waitFor(() => {
|
||||
expect(setQuerySpy).toBeCalledWith(expectedQuery);
|
||||
expect(setTimeSpy).toBeCalledWith(expectedTimeRange);
|
||||
expect(setRefreshIntervalSpy).toBeCalledWith(refreshInterval);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -208,6 +208,8 @@ export function MockApmPluginContextWrapper({
|
|||
createCallApmApi(contextValue.core);
|
||||
}
|
||||
|
||||
performance.mark = jest.fn();
|
||||
|
||||
const contextHistory = useHistory();
|
||||
|
||||
const usedHistory = useMemo(() => {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { usePerformanceContext } from '@kbn/ebt-tools';
|
||||
import { useEuiTheme, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana';
|
||||
|
@ -24,6 +25,7 @@ export const UnifiedSearchBar = () => {
|
|||
const { metricsView } = useMetricsDataViewContext();
|
||||
const { searchCriteria, onLimitChange, onPanelFiltersChange, onSubmit } =
|
||||
useUnifiedSearchContext();
|
||||
const { onPageRefreshStart } = usePerformanceContext();
|
||||
|
||||
const { SearchBar } = unifiedSearch.ui;
|
||||
|
||||
|
@ -32,9 +34,10 @@ export const UnifiedSearchBar = () => {
|
|||
// This makes sure `onSubmit` is only called when the submit button is clicked
|
||||
if (isUpdate === false) {
|
||||
onSubmit(payload);
|
||||
onPageRefreshStart();
|
||||
}
|
||||
},
|
||||
[onSubmit]
|
||||
[onSubmit, onPageRefreshStart]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { PerformanceContextProvider } from '@kbn/ebt-tools';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useMetricsExplorerState } from './use_metric_explorer_state';
|
||||
import { MetricsExplorerOptionsContainer } from './use_metrics_explorer_options';
|
||||
import React from 'react';
|
||||
|
@ -16,6 +18,11 @@ jest.mock('../../../../hooks/use_kibana_timefilter_time', () => ({
|
|||
useSyncKibanaTimeFilterTime: () => [() => {}],
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../alerting/use_alert_prefill', () => ({
|
||||
useAlertPrefillContext: () => ({
|
||||
metricThresholdPrefill: {
|
||||
|
@ -27,7 +34,9 @@ jest.mock('../../../../alerting/use_alert_prefill', () => ({
|
|||
const renderUseMetricsExplorerStateHook = () =>
|
||||
renderHook(() => useMetricsExplorerState(), {
|
||||
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
|
||||
<MetricsExplorerOptionsContainer>{children}</MetricsExplorerOptionsContainer>
|
||||
<PerformanceContextProvider>
|
||||
<MetricsExplorerOptionsContainer>{children}</MetricsExplorerOptionsContainer>
|
||||
</PerformanceContextProvider>
|
||||
),
|
||||
});
|
||||
|
||||
|
@ -73,6 +82,12 @@ describe('useMetricsExplorerState', () => {
|
|||
});
|
||||
delete STORE.MetricsExplorerOptions;
|
||||
delete STORE.MetricsExplorerTimeRange;
|
||||
|
||||
const pathname = '/hosts';
|
||||
(useLocation as jest.Mock).mockReturnValue(() => ({
|
||||
pathname,
|
||||
}));
|
||||
performance.mark = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -88,9 +103,11 @@ describe('useMetricsExplorerState', () => {
|
|||
},
|
||||
});
|
||||
const { result } = renderUseMetricsExplorerStateHook();
|
||||
expect(result.current.data!.pages[0]).toEqual(resp);
|
||||
expect(result.current.error).toBe(null);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
await waitFor(() => {
|
||||
expect(result.current.data!.pages[0]).toEqual(resp);
|
||||
expect(result.current.error).toBe(null);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleRefresh', () => {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import DateMath from '@kbn/datemath';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { usePerformanceContext } from '@kbn/ebt-tools';
|
||||
import type {
|
||||
MetricsExplorerChartOptions,
|
||||
MetricsExplorerOptions,
|
||||
|
@ -35,6 +36,7 @@ export const useMetricsExplorerState = ({ enabled }: { enabled: boolean } = { en
|
|||
timestamps,
|
||||
setTimestamps,
|
||||
} = useMetricsExplorerOptionsContainerContext();
|
||||
const { onPageRefreshStart } = usePerformanceContext();
|
||||
|
||||
const refreshTimestamps = useCallback(() => {
|
||||
const fromTimestamp = DateMath.parse(timeRange.from)!.valueOf();
|
||||
|
@ -45,7 +47,8 @@ export const useMetricsExplorerState = ({ enabled }: { enabled: boolean } = { en
|
|||
fromTimestamp,
|
||||
toTimestamp,
|
||||
});
|
||||
}, [setTimestamps, timeRange]);
|
||||
onPageRefreshStart();
|
||||
}, [setTimestamps, timeRange, onPageRefreshStart]);
|
||||
|
||||
const { data, error, fetchNextPage, isLoading } = useMetricsExplorerData({
|
||||
options,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useTrackPageview, FeatureFeedbackButton } from '@kbn/observability-shared-plugin/public';
|
||||
import { usePerformanceContext } from '@kbn/ebt-tools';
|
||||
import { OnboardingFlow } from '../../../components/shared/templates/no_data_config';
|
||||
|
@ -65,7 +65,7 @@ const MetricsExplorerContent = () => {
|
|||
const { currentView } = useMetricsExplorerViews();
|
||||
|
||||
const { kibanaVersion, isCloudEnv, isServerlessEnv } = useKibanaEnvironmentContext();
|
||||
|
||||
const prevDataRef = useRef(data);
|
||||
const { onPageReady } = usePerformanceContext();
|
||||
|
||||
useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer' });
|
||||
|
@ -96,14 +96,18 @@ const MetricsExplorerContent = () => {
|
|||
currentTimerange: timeRange,
|
||||
};
|
||||
|
||||
if (!isLoading) {
|
||||
onPageReady({
|
||||
meta: {
|
||||
rangeFrom: timeRange.from,
|
||||
rangeTo: timeRange.to,
|
||||
},
|
||||
});
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!isLoading && data && prevDataRef.current !== data) {
|
||||
onPageReady({
|
||||
meta: {
|
||||
rangeFrom: timeRange.from,
|
||||
rangeTo: timeRange.to,
|
||||
},
|
||||
});
|
||||
|
||||
prevDataRef.current = data;
|
||||
}
|
||||
}, [isLoading, data, timeRange.from, timeRange.to, onPageReady]);
|
||||
|
||||
return (
|
||||
<InfraPageTemplate
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue