mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Performance] Refactor TTFMP query `from`, `to` fields (#213911)](https://github.com/elastic/kibana/pull/213911) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Abdul Wahab Zahid","email":"awahab07@yahoo.com"},"sourceCommit":{"committedDate":"2025-03-20T10:40:24Z","message":"[Performance] Refactor TTFMP query `from`, `to` fields (#213911)\n\nCurrently Kibana forwards `query_range_secs` and `query_offset_secs` to\nmark the selected time range when reporting TTFMP event. This format\ncaused some challenges to identify `from`, `to` date offsets in\nvisualizations.\n\nTo simplify, the PR renames and sends the three fields explicitly:\n- `query_from_offset_secs` offset to `0` (now), with -ve for past and\n+ve for future dates\n- `query_to_offset_secs` offset to `0` (now), with -ve for past and +ve\nfor future dates\n- `query_range_secs` same as previously sent\n\n_This approach is followed after a discussion, and based on the\n[gist](https://gist.github.com/andrewvc/1f04a57a336d768e4ec5ff2eff06ba54)\nexcerpt:_\n\n```\nEarliest date -> QueryFrom\nNewest date -> QueryTo\nDuration -> QueryRange\n```\n\n### Indexing\nThese fields then should be mapped in the EBT indexer to ingest in the\ntop level of the document, eventually removing the need to create\nruntime fields in data views for visualizations.\n\nAlso, runtime fields in data views should be updated to reflect this\nchange. For backward compatibility, the runtime fields can cater both\nthe old and new field names conditionally.\n\n### Testing\n- Ensure that the TTFMP events are correctly reporting the date ranges.\n\n### Example\n\n","sha":"e6e78ac6d83fe9c4a83785c717fb1b7f3fedbf0e","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport missing","v9.1.0","v8.19.0"],"title":"[Performance] Refactor TTFMP query `from`, `to` fields","number":213911,"url":"https://github.com/elastic/kibana/pull/213911","mergeCommit":{"message":"[Performance] Refactor TTFMP query `from`, `to` fields (#213911)\n\nCurrently Kibana forwards `query_range_secs` and `query_offset_secs` to\nmark the selected time range when reporting TTFMP event. This format\ncaused some challenges to identify `from`, `to` date offsets in\nvisualizations.\n\nTo simplify, the PR renames and sends the three fields explicitly:\n- `query_from_offset_secs` offset to `0` (now), with -ve for past and\n+ve for future dates\n- `query_to_offset_secs` offset to `0` (now), with -ve for past and +ve\nfor future dates\n- `query_range_secs` same as previously sent\n\n_This approach is followed after a discussion, and based on the\n[gist](https://gist.github.com/andrewvc/1f04a57a336d768e4ec5ff2eff06ba54)\nexcerpt:_\n\n```\nEarliest date -> QueryFrom\nNewest date -> QueryTo\nDuration -> QueryRange\n```\n\n### Indexing\nThese fields then should be mapped in the EBT indexer to ingest in the\ntop level of the document, eventually removing the need to create\nruntime fields in data views for visualizations.\n\nAlso, runtime fields in data views should be updated to reflect this\nchange. For backward compatibility, the runtime fields can cater both\nthe old and new field names conditionally.\n\n### Testing\n- Ensure that the TTFMP events are correctly reporting the date ranges.\n\n### Example\n\n","sha":"e6e78ac6d83fe9c4a83785c717fb1b7f3fedbf0e"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/213911","number":213911,"mergeCommit":{"message":"[Performance] Refactor TTFMP query `from`, `to` fields (#213911)\n\nCurrently Kibana forwards `query_range_secs` and `query_offset_secs` to\nmark the selected time range when reporting TTFMP event. This format\ncaused some challenges to identify `from`, `to` date offsets in\nvisualizations.\n\nTo simplify, the PR renames and sends the three fields explicitly:\n- `query_from_offset_secs` offset to `0` (now), with -ve for past and\n+ve for future dates\n- `query_to_offset_secs` offset to `0` (now), with -ve for past and +ve\nfor future dates\n- `query_range_secs` same as previously sent\n\n_This approach is followed after a discussion, and based on the\n[gist](https://gist.github.com/andrewvc/1f04a57a336d768e4ec5ff2eff06ba54)\nexcerpt:_\n\n```\nEarliest date -> QueryFrom\nNewest date -> QueryTo\nDuration -> QueryRange\n```\n\n### Indexing\nThese fields then should be mapped in the EBT indexer to ingest in the\ntop level of the document, eventually removing the need to create\nruntime fields in data views for visualizations.\n\nAlso, runtime fields in data views should be updated to reflect this\nchange. For backward compatibility, the runtime fields can cater both\nthe old and new field names conditionally.\n\n### Testing\n- Ensure that the TTFMP events are correctly reporting the date ranges.\n\n### Example\n\n","sha":"e6e78ac6d83fe9c4a83785c717fb1b7f3fedbf0e"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT-->
This commit is contained in:
parent
bbe76886d0
commit
a6e6eddf47
17 changed files with 456 additions and 169 deletions
|
@ -330,8 +330,9 @@ This will be indexed as:
|
|||
"duration": 736, // Event duration as specified when reporting it
|
||||
"meta": {
|
||||
"target": '/home',
|
||||
"query_range_secs": 900
|
||||
"query_offset_secs": 0 // now
|
||||
"query_range_secs": 900, // 15 minutes
|
||||
"query_from_offset_secs": -900 // From 15 minutes ago
|
||||
"query_to_offset_secs": 0 // To now
|
||||
},
|
||||
"context": { // Context holds information identifying the deployment, version, application and page that generated the event
|
||||
"version": "8.16.0-SNAPSHOT",
|
||||
|
|
|
@ -114,6 +114,12 @@ describe('trackPerformanceMeasureEntries', () => {
|
|||
anyKey: 'anyKey',
|
||||
anyValue: 'anyValue',
|
||||
},
|
||||
meta: {
|
||||
isInitialLoad: true,
|
||||
queryRangeSecs: 86400,
|
||||
queryFromOffsetSecs: -86400,
|
||||
queryToOffsetSecs: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
@ -124,7 +130,12 @@ describe('trackPerformanceMeasureEntries', () => {
|
|||
duration: 1000,
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
key1: 'key1',
|
||||
meta: { target: '/' },
|
||||
meta: {
|
||||
is_initial_load: true,
|
||||
query_range_secs: 86400,
|
||||
query_from_offset_secs: -86400,
|
||||
query_to_offset_secs: 0,
|
||||
},
|
||||
value1: 'value1',
|
||||
});
|
||||
});
|
||||
|
@ -141,7 +152,8 @@ describe('trackPerformanceMeasureEntries', () => {
|
|||
type: 'kibana:performance',
|
||||
meta: {
|
||||
queryRangeSecs: 86400,
|
||||
queryOffsetSecs: 0,
|
||||
queryFromOffsetSecs: -86400,
|
||||
queryToOffsetSecs: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -152,7 +164,84 @@ 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_from_offset_secs: -86400,
|
||||
query_to_offset_secs: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('reports an analytics event with description metadata', () => {
|
||||
setupMockPerformanceObserver([
|
||||
{
|
||||
name: '/',
|
||||
entryType: 'measure',
|
||||
startTime: 100,
|
||||
duration: 1000,
|
||||
detail: {
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
meta: {
|
||||
isInitialLoad: false,
|
||||
description:
|
||||
'[ttfmp_dependencies] onPageReady is called when the most important content is rendered',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
trackPerformanceMeasureEntries(analyticsClientMock, true);
|
||||
|
||||
expect(analyticsClientMock.reportEvent).toHaveBeenCalledTimes(1);
|
||||
expect(analyticsClientMock.reportEvent).toHaveBeenCalledWith('performance_metric', {
|
||||
duration: 1000,
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
meta: {
|
||||
is_initial_load: false,
|
||||
query_range_secs: undefined,
|
||||
query_from_offset_secs: undefined,
|
||||
query_to_offset_secs: undefined,
|
||||
description:
|
||||
'[ttfmp_dependencies] onPageReady is called when the most important content is rendered',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('reports an analytics event with truncated description metadata', () => {
|
||||
setupMockPerformanceObserver([
|
||||
{
|
||||
name: '/',
|
||||
entryType: 'measure',
|
||||
startTime: 100,
|
||||
duration: 1000,
|
||||
detail: {
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
meta: {
|
||||
isInitialLoad: false,
|
||||
description:
|
||||
'[ttfmp_dependencies] This is a very long long long long long long long long description. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque non risus in nunc tincidunt tincidunt. Proin vehicula, nunc at feugiat cursus, justo nulla fermentum lorem, non ultricies metus libero nec purus. Sed ut perspiciatis unde omnis iste natus.',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
trackPerformanceMeasureEntries(analyticsClientMock, true);
|
||||
const truncatedDescription =
|
||||
'[ttfmp_dependencies] This is a very long long long long long long long long description. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque non risus in nunc tincidunt tincidunt. Proin vehicula, nunc at feugiat cursus, justo nulla fermentum l';
|
||||
|
||||
expect(analyticsClientMock.reportEvent).toHaveBeenCalledTimes(1);
|
||||
expect(analyticsClientMock.reportEvent).toHaveBeenCalledWith('performance_metric', {
|
||||
duration: 1000,
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
meta: {
|
||||
is_initial_load: false,
|
||||
query_range_secs: undefined,
|
||||
query_from_offset_secs: undefined,
|
||||
query_to_offset_secs: undefined,
|
||||
description: truncatedDescription,
|
||||
},
|
||||
});
|
||||
|
||||
expect(truncatedDescription.length).toBe(256);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import type { AnalyticsClient } from '@elastic/ebt/client';
|
|||
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
||||
|
||||
const MAX_CUSTOM_METRICS = 9;
|
||||
const MAX_DESCRIPTION_LENGTH = 256;
|
||||
// The keys and values for the custom metrics are limited to 9 pairs
|
||||
const ALLOWED_CUSTOM_METRICS_KEYS_VALUES = Array.from({ length: MAX_CUSTOM_METRICS }, (_, i) => [
|
||||
`key${i + 1}`,
|
||||
|
@ -28,6 +29,8 @@ export function trackPerformanceMeasureEntries(analytics: AnalyticsClient, isDev
|
|||
const target = entry?.name;
|
||||
const duration = entry.duration;
|
||||
const meta = entry.detail?.meta;
|
||||
const description = meta?.description;
|
||||
|
||||
const customMetrics = Object.keys(entry.detail?.customMetrics ?? {}).reduce(
|
||||
(acc, metric) => {
|
||||
if (ALLOWED_CUSTOM_METRICS_KEYS_VALUES.includes(metric)) {
|
||||
|
@ -55,6 +58,13 @@ export function trackPerformanceMeasureEntries(analytics: AnalyticsClient, isDev
|
|||
);
|
||||
}
|
||||
|
||||
if (description?.length > MAX_DESCRIPTION_LENGTH) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`The description for the measure: ${target} is too long. The maximum length is ${MAX_DESCRIPTION_LENGTH}. Strings longer than ${MAX_DESCRIPTION_LENGTH} will not be indexed or stored`
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`The measure ${target} completed in ${duration / 1000}s`);
|
||||
}
|
||||
|
@ -72,9 +82,11 @@ export function trackPerformanceMeasureEntries(analytics: AnalyticsClient, isDev
|
|||
duration,
|
||||
...customMetrics,
|
||||
meta: {
|
||||
target,
|
||||
query_range_secs: meta?.queryRangeSecs,
|
||||
query_offset_secs: meta?.queryOffsetSecs,
|
||||
query_from_offset_secs: meta?.queryFromOffsetSecs,
|
||||
query_to_offset_secs: meta?.queryToOffsetSecs,
|
||||
description: description?.slice(0, MAX_DESCRIPTION_LENGTH),
|
||||
is_initial_load: meta?.isInitialLoad,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
@ -12,28 +12,32 @@ import {
|
|||
getOffsetFromNowInSeconds,
|
||||
getTimeDifferenceInSeconds,
|
||||
} from '@kbn/timerange';
|
||||
import { perfomanceMarkers } from '../../performance_markers';
|
||||
import { EventData } from '../performance_context';
|
||||
import { perfomanceMarkers } from '../../performance_markers';
|
||||
import { DescriptionWithPrefix } from '../types';
|
||||
|
||||
interface PerformanceMeta {
|
||||
queryRangeSecs: number;
|
||||
queryOffsetSecs: number;
|
||||
queryRangeSecs?: number;
|
||||
queryFromOffsetSecs?: number;
|
||||
queryToOffsetSecs?: number;
|
||||
isInitialLoad?: boolean;
|
||||
description?: DescriptionWithPrefix;
|
||||
}
|
||||
|
||||
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) {
|
||||
let performanceMeta: PerformanceMeta | undefined;
|
||||
pageReady(eventData?: EventData) {
|
||||
const performanceMeta: PerformanceMeta = {};
|
||||
performance.mark(perfomanceMarkers.endPageReady);
|
||||
|
||||
if (eventData?.meta) {
|
||||
if (eventData?.meta?.rangeFrom && eventData?.meta?.rangeTo) {
|
||||
const { rangeFrom, rangeTo } = eventData.meta;
|
||||
|
||||
// Convert the date range to epoch timestamps (in milliseconds)
|
||||
|
@ -42,26 +46,59 @@ export function measureInteraction() {
|
|||
to: rangeTo,
|
||||
});
|
||||
|
||||
performanceMeta = {
|
||||
queryRangeSecs: getTimeDifferenceInSeconds(dateRangesInEpoch),
|
||||
queryOffsetSecs:
|
||||
rangeTo === 'now' ? 0 : getOffsetFromNowInSeconds(dateRangesInEpoch.endDate),
|
||||
};
|
||||
performanceMeta.queryRangeSecs = getTimeDifferenceInSeconds(dateRangesInEpoch);
|
||||
performanceMeta.queryFromOffsetSecs =
|
||||
rangeFrom === 'now' ? 0 : getOffsetFromNowInSeconds(dateRangesInEpoch.startDate);
|
||||
performanceMeta.queryToOffsetSecs =
|
||||
rangeTo === 'now' ? 0 : getOffsetFromNowInSeconds(dateRangesInEpoch.endDate);
|
||||
}
|
||||
|
||||
if (!trackedRoutes.includes(pathname)) {
|
||||
performance.measure(pathname, {
|
||||
if (eventData?.meta?.description) {
|
||||
performanceMeta.description = eventData.meta.description;
|
||||
}
|
||||
|
||||
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,201 @@ 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,
|
||||
queryFromOffsetSecs: -900,
|
||||
queryToOffsetSecs: 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,
|
||||
queryFromOffsetSecs: -1800,
|
||||
queryToOffsetSecs: 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,
|
||||
queryFromOffsetSecs: -88200,
|
||||
queryToOffsetSecs: -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,
|
||||
queryFromOffsetSecs: -84600,
|
||||
queryToOffsetSecs: 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,
|
||||
queryFromOffsetSecs: -84600,
|
||||
queryToOffsetSecs: 1800,
|
||||
isInitialLoad: false,
|
||||
},
|
||||
},
|
||||
end: 'end::pageReady',
|
||||
start: 'start::pageRefresh',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,13 +13,15 @@ import { useLocation } from 'react-router-dom';
|
|||
import { PerformanceApi, PerformanceContext } from './use_performance_context';
|
||||
import { PerformanceMetricEvent } from '../../performance_metric_events';
|
||||
import { measureInteraction } from './measure_interaction';
|
||||
|
||||
import { DescriptionWithPrefix } from './types';
|
||||
export type CustomMetrics = Omit<PerformanceMetricEvent, 'eventName' | 'meta' | 'duration'>;
|
||||
|
||||
export interface Meta {
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
description?: DescriptionWithPrefix;
|
||||
}
|
||||
|
||||
export interface EventData {
|
||||
customMetrics?: CustomMetrics;
|
||||
meta?: Meta;
|
||||
|
@ -28,7 +30,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 +47,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>;
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
type ApmPageId = 'services' | 'traces' | 'dependencies';
|
||||
type InfraPageId = 'hosts';
|
||||
type OnboardingPageId = 'onboarding';
|
||||
|
||||
export type Key = `${ApmPageId}` | `${InfraPageId}` | `${OnboardingPageId}`;
|
||||
|
||||
export type DescriptionWithPrefix = `[ttfmp_${Key}] ${string}`;
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -218,6 +218,9 @@ export function MockApmPluginContextWrapper({
|
|||
createCallApmApi(contextValue.core);
|
||||
}
|
||||
|
||||
performance.mark = jest.fn();
|
||||
performance.clearMeasures = 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,13 @@ describe('useMetricsExplorerState', () => {
|
|||
});
|
||||
delete STORE.MetricsExplorerOptions;
|
||||
delete STORE.MetricsExplorerTimeRange;
|
||||
|
||||
const pathname = '/hosts';
|
||||
(useLocation as jest.Mock).mockReturnValue(() => ({
|
||||
pathname,
|
||||
}));
|
||||
performance.mark = jest.fn();
|
||||
performance.clearMeasures = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -88,9 +104,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,8 +6,9 @@
|
|||
*/
|
||||
|
||||
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';
|
||||
import { InfraPageTemplate } from '../../../components/shared/templates/infra_page_template';
|
||||
import { WithMetricsExplorerOptionsUrlState } from '../../../containers/metrics_explorer/with_metrics_explorer_options_url_state';
|
||||
|
@ -64,6 +65,8 @@ const MetricsExplorerContent = () => {
|
|||
const { currentView } = useMetricsExplorerViews();
|
||||
|
||||
const { kibanaVersion, isCloudEnv, isServerlessEnv } = useKibanaEnvironmentContext();
|
||||
const prevDataRef = useRef(data);
|
||||
const { onPageReady } = usePerformanceContext();
|
||||
|
||||
useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer' });
|
||||
useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer', delay: 15000 });
|
||||
|
@ -93,6 +96,19 @@ const MetricsExplorerContent = () => {
|
|||
currentTimerange: timeRange,
|
||||
};
|
||||
|
||||
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
|
||||
onboardingFlow={OnboardingFlow.Infra}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue