mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Perfomance] Track time range picker with `onPageReady` function (#202889)](https://github.com/elastic/kibana/pull/202889) <!--- Backport version: 8.9.8 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Katerina","email":"aikaterini.patticha@elastic.co"},"sourceCommit":{"committedDate":"2024-12-13T13:11:44Z","message":"[Perfomance] Track time range picker with `onPageReady` function (#202889)\n\n## Summary\r\n\r\ncloses https://github.com/elastic/observability-dev/issues/3377\r\n## Metrics \r\n#### `meta.query_range_secs` - The duration of the selected time range\r\nin seconds.\r\n#### `meta.query_offset_secs` - The offset from \"now\" to the\r\n'rangeTo'/end' time picker value in seconds.\r\n\r\n____\r\n\r\nExtend the `onPageReady` function to support date ranges in the meta\r\nfield. The function should compute the query range in seconds based on\r\nthe provided time range and report it to telemetry as\r\nmeta.query_range_secs.\r\n\r\n\r\n\r\n\r\nIf the `rangeTo` is different from 'now', calculate the offset. \r\n- A negative offset indicates that the rangeTo is in the past, \r\n- a positive offset means it is in the future, \r\n- and zero indicates that the rangeTo is exactly 'now'.\" \r\n\r\n\r\n\r\n### How to instrument\r\nTo report the selected time range, pass the `rangeFrom` and `rangeTo` . \r\n> Failing to pass the correct type will result in TS error.\r\n\r\n\r\nThen, use this data when invoking onPageReady:\r\n```\r\n onPageReady({\r\n meta: { rangeFrom, rangeTo },\r\n });\r\n```\r\n\r\n### Analysis \r\n\r\nMeta is flatten field. In order to aggregate the data it's necessary to\r\ncreate a run time field. You can add a field in the\r\n\r\n1. select data view (`ebt-kibana-*-performance-metrics`) \r\n2. Add a new field\r\n3. Type double\r\n4. Set value \r\n\r\n`query_range_secs`\r\n```\r\ndef meta = doc[“meta”].size();\r\nif (meta > 0) {\r\n def range = doc[“meta.query_range_secs”].size();\r\n if (range > 0) {\r\n // Emit the value of ‘meta.target’\r\n emit(Double.parseDouble(doc[“meta.query_range_secs”].value));\r\n }\r\n}\r\n\r\n```\r\n\r\n`query_offset_secs` \r\n```\r\n\r\ndef meta = doc[“meta”].size();\r\nif (meta > 0) {\r\n def offset = doc[“meta.query_offset_secs”].size();\r\n if (offset > 0) {\r\n \r\n emit(Double.parseDouble(doc[“meta.query_offset_secs”].value));\r\n }\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n### Examples\r\n\r\n\r\n<img width=\"1478\" alt=\"Screenshot 2024-12-09 at 19 51 32\"\r\nsrc=\"https://github.com/user-attachments/assets/72f796e1-4f20-487f-b62a-b6a4aead9a4a\">\r\n\r\n<img width=\"1478\" alt=\"Screenshot 2024-12-09 at 19 56 08\"\r\nsrc=\"https://github.com/user-attachments/assets/c278dc3b-e6f3-47ed-9c90-954d71b59161\">\r\n\r\n<img width=\"1478\" alt=\"Screenshot 2024-12-09 at 19 53 45 1\"\r\nsrc=\"https://github.com/user-attachments/assets/ef42ecef-48cd-4396-9f5d-c971098d5219\">\r\n\r\n\r\n\r\n\r\n\r\n### Notes\r\n- Instrumented only 2 solutions as an example (dataset and apm services)\r\n\r\n### TODO\r\n- [x] Update documentation -\r\nhttps://github.com/elastic/kibana/pull/204179\r\n- [ ] Update dashboards (create a runtime field) \r\n- [x] Track offset ( we need to know if the user selected now or now)\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"bd1c00fd65848ff27a1bace14363c5ab326c491d","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport missing","v9.0.0","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-infra_services"],"number":202889,"url":"https://github.com/elastic/kibana/pull/202889","mergeCommit":{"message":"[Perfomance] Track time range picker with `onPageReady` function (#202889)\n\n## Summary\r\n\r\ncloses https://github.com/elastic/observability-dev/issues/3377\r\n## Metrics \r\n#### `meta.query_range_secs` - The duration of the selected time range\r\nin seconds.\r\n#### `meta.query_offset_secs` - The offset from \"now\" to the\r\n'rangeTo'/end' time picker value in seconds.\r\n\r\n____\r\n\r\nExtend the `onPageReady` function to support date ranges in the meta\r\nfield. The function should compute the query range in seconds based on\r\nthe provided time range and report it to telemetry as\r\nmeta.query_range_secs.\r\n\r\n\r\n\r\n\r\nIf the `rangeTo` is different from 'now', calculate the offset. \r\n- A negative offset indicates that the rangeTo is in the past, \r\n- a positive offset means it is in the future, \r\n- and zero indicates that the rangeTo is exactly 'now'.\" \r\n\r\n\r\n\r\n### How to instrument\r\nTo report the selected time range, pass the `rangeFrom` and `rangeTo` . \r\n> Failing to pass the correct type will result in TS error.\r\n\r\n\r\nThen, use this data when invoking onPageReady:\r\n```\r\n onPageReady({\r\n meta: { rangeFrom, rangeTo },\r\n });\r\n```\r\n\r\n### Analysis \r\n\r\nMeta is flatten field. In order to aggregate the data it's necessary to\r\ncreate a run time field. You can add a field in the\r\n\r\n1. select data view (`ebt-kibana-*-performance-metrics`) \r\n2. Add a new field\r\n3. Type double\r\n4. Set value \r\n\r\n`query_range_secs`\r\n```\r\ndef meta = doc[“meta”].size();\r\nif (meta > 0) {\r\n def range = doc[“meta.query_range_secs”].size();\r\n if (range > 0) {\r\n // Emit the value of ‘meta.target’\r\n emit(Double.parseDouble(doc[“meta.query_range_secs”].value));\r\n }\r\n}\r\n\r\n```\r\n\r\n`query_offset_secs` \r\n```\r\n\r\ndef meta = doc[“meta”].size();\r\nif (meta > 0) {\r\n def offset = doc[“meta.query_offset_secs”].size();\r\n if (offset > 0) {\r\n \r\n emit(Double.parseDouble(doc[“meta.query_offset_secs”].value));\r\n }\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n### Examples\r\n\r\n\r\n<img width=\"1478\" alt=\"Screenshot 2024-12-09 at 19 51 32\"\r\nsrc=\"https://github.com/user-attachments/assets/72f796e1-4f20-487f-b62a-b6a4aead9a4a\">\r\n\r\n<img width=\"1478\" alt=\"Screenshot 2024-12-09 at 19 56 08\"\r\nsrc=\"https://github.com/user-attachments/assets/c278dc3b-e6f3-47ed-9c90-954d71b59161\">\r\n\r\n<img width=\"1478\" alt=\"Screenshot 2024-12-09 at 19 53 45 1\"\r\nsrc=\"https://github.com/user-attachments/assets/ef42ecef-48cd-4396-9f5d-c971098d5219\">\r\n\r\n\r\n\r\n\r\n\r\n### Notes\r\n- Instrumented only 2 solutions as an example (dataset and apm services)\r\n\r\n### TODO\r\n- [x] Update documentation -\r\nhttps://github.com/elastic/kibana/pull/204179\r\n- [ ] Update dashboards (create a runtime field) \r\n- [x] Track offset ( we need to know if the user selected now or now)\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"bd1c00fd65848ff27a1bace14363c5ab326c491d"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/202889","number":202889,"mergeCommit":{"message":"[Perfomance] Track time range picker with `onPageReady` function (#202889)\n\n## Summary\r\n\r\ncloses https://github.com/elastic/observability-dev/issues/3377\r\n## Metrics \r\n#### `meta.query_range_secs` - The duration of the selected time range\r\nin seconds.\r\n#### `meta.query_offset_secs` - The offset from \"now\" to the\r\n'rangeTo'/end' time picker value in seconds.\r\n\r\n____\r\n\r\nExtend the `onPageReady` function to support date ranges in the meta\r\nfield. The function should compute the query range in seconds based on\r\nthe provided time range and report it to telemetry as\r\nmeta.query_range_secs.\r\n\r\n\r\n\r\n\r\nIf the `rangeTo` is different from 'now', calculate the offset. \r\n- A negative offset indicates that the rangeTo is in the past, \r\n- a positive offset means it is in the future, \r\n- and zero indicates that the rangeTo is exactly 'now'.\" \r\n\r\n\r\n\r\n### How to instrument\r\nTo report the selected time range, pass the `rangeFrom` and `rangeTo` . \r\n> Failing to pass the correct type will result in TS error.\r\n\r\n\r\nThen, use this data when invoking onPageReady:\r\n```\r\n onPageReady({\r\n meta: { rangeFrom, rangeTo },\r\n });\r\n```\r\n\r\n### Analysis \r\n\r\nMeta is flatten field. In order to aggregate the data it's necessary to\r\ncreate a run time field. You can add a field in the\r\n\r\n1. select data view (`ebt-kibana-*-performance-metrics`) \r\n2. Add a new field\r\n3. Type double\r\n4. Set value \r\n\r\n`query_range_secs`\r\n```\r\ndef meta = doc[“meta”].size();\r\nif (meta > 0) {\r\n def range = doc[“meta.query_range_secs”].size();\r\n if (range > 0) {\r\n // Emit the value of ‘meta.target’\r\n emit(Double.parseDouble(doc[“meta.query_range_secs”].value));\r\n }\r\n}\r\n\r\n```\r\n\r\n`query_offset_secs` \r\n```\r\n\r\ndef meta = doc[“meta”].size();\r\nif (meta > 0) {\r\n def offset = doc[“meta.query_offset_secs”].size();\r\n if (offset > 0) {\r\n \r\n emit(Double.parseDouble(doc[“meta.query_offset_secs”].value));\r\n }\r\n}\r\n\r\n```\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n### Examples\r\n\r\n\r\n<img width=\"1478\" alt=\"Screenshot 2024-12-09 at 19 51 32\"\r\nsrc=\"https://github.com/user-attachments/assets/72f796e1-4f20-487f-b62a-b6a4aead9a4a\">\r\n\r\n<img width=\"1478\" alt=\"Screenshot 2024-12-09 at 19 56 08\"\r\nsrc=\"https://github.com/user-attachments/assets/c278dc3b-e6f3-47ed-9c90-954d71b59161\">\r\n\r\n<img width=\"1478\" alt=\"Screenshot 2024-12-09 at 19 53 45 1\"\r\nsrc=\"https://github.com/user-attachments/assets/ef42ecef-48cd-4396-9f5d-c971098d5219\">\r\n\r\n\r\n\r\n\r\n\r\n### Notes\r\n- Instrumented only 2 solutions as an example (dataset and apm services)\r\n\r\n### TODO\r\n- [x] Update documentation -\r\nhttps://github.com/elastic/kibana/pull/204179\r\n- [ ] Update dashboards (create a runtime field) \r\n- [x] Track offset ( we need to know if the user selected now or now)\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"bd1c00fd65848ff27a1bace14363c5ab326c491d"}}]}] BACKPORT-->
This commit is contained in:
parent
16f4187ab3
commit
64de0709de
18 changed files with 398 additions and 58 deletions
|
@ -128,4 +128,31 @@ describe('trackPerformanceMeasureEntries', () => {
|
|||
value1: 'value1',
|
||||
});
|
||||
});
|
||||
|
||||
test('reports an analytics event with query metadata', () => {
|
||||
setupMockPerformanceObserver([
|
||||
{
|
||||
name: '/',
|
||||
entryType: 'measure',
|
||||
startTime: 100,
|
||||
duration: 1000,
|
||||
detail: {
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
meta: {
|
||||
queryRangeSecs: 86400,
|
||||
queryOffsetSecs: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
trackPerformanceMeasureEntries(analyticsClientMock, true);
|
||||
|
||||
expect(analyticsClientMock.reportEvent).toHaveBeenCalledTimes(1);
|
||||
expect(analyticsClientMock.reportEvent).toHaveBeenCalledWith('performance_metric', {
|
||||
duration: 1000,
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
meta: { target: '/', query_range_secs: 86400, query_offset_secs: 0 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,6 +27,7 @@ export function trackPerformanceMeasureEntries(analytics: AnalyticsClient, isDev
|
|||
if (entry.entryType === 'measure' && entry.detail?.type === 'kibana:performance') {
|
||||
const target = entry?.name;
|
||||
const duration = entry.duration;
|
||||
const meta = entry.detail?.meta;
|
||||
const customMetrics = Object.keys(entry.detail?.customMetrics ?? {}).reduce(
|
||||
(acc, metric) => {
|
||||
if (ALLOWED_CUSTOM_METRICS_KEYS_VALUES.includes(metric)) {
|
||||
|
@ -72,6 +73,8 @@ export function trackPerformanceMeasureEntries(analytics: AnalyticsClient, isDev
|
|||
...customMetrics,
|
||||
meta: {
|
||||
target,
|
||||
query_range_secs: meta?.queryRangeSecs,
|
||||
query_offset_secs: meta?.queryOffsetSecs,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
@ -25,6 +25,7 @@ SHARED_DEPS = [
|
|||
"@npm//@elastic/apm-rum-core",
|
||||
"@npm//react",
|
||||
"@npm//react-router-dom",
|
||||
"//packages/kbn-timerange"
|
||||
]
|
||||
|
||||
js_library(
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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".
|
||||
*/
|
||||
|
||||
import {
|
||||
getDateRange,
|
||||
getOffsetFromNowInSeconds,
|
||||
getTimeDifferenceInSeconds,
|
||||
} from '@kbn/timerange';
|
||||
import { perfomanceMarkers } from '../../performance_markers';
|
||||
import { EventData } from '../performance_context';
|
||||
|
||||
interface PerformanceMeta {
|
||||
queryRangeSecs: number;
|
||||
queryOffsetSecs: number;
|
||||
}
|
||||
|
||||
export function measureInteraction() {
|
||||
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;
|
||||
performance.mark(perfomanceMarkers.endPageReady);
|
||||
|
||||
if (eventData?.meta) {
|
||||
const { rangeFrom, rangeTo } = eventData.meta;
|
||||
|
||||
// Convert the date range to epoch timestamps (in milliseconds)
|
||||
const dateRangesInEpoch = getDateRange({
|
||||
from: rangeFrom,
|
||||
to: rangeTo,
|
||||
});
|
||||
|
||||
performanceMeta = {
|
||||
queryRangeSecs: getTimeDifferenceInSeconds(dateRangesInEpoch),
|
||||
queryOffsetSecs:
|
||||
rangeTo === 'now' ? 0 : getOffsetFromNowInSeconds(dateRangesInEpoch.endDate),
|
||||
};
|
||||
}
|
||||
|
||||
if (!trackedRoutes.includes(pathname)) {
|
||||
performance.measure(pathname, {
|
||||
detail: {
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
customMetrics: eventData?.customMetrics,
|
||||
meta: performanceMeta,
|
||||
},
|
||||
start: perfomanceMarkers.startPageChange,
|
||||
end: perfomanceMarkers.endPageReady,
|
||||
});
|
||||
trackedRoutes.push(pathname);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* 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".
|
||||
*/
|
||||
|
||||
import { measureInteraction } from '.';
|
||||
import { perfomanceMarkers } from '../../performance_markers';
|
||||
|
||||
describe('measureInteraction', () => {
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
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' },
|
||||
};
|
||||
|
||||
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: eventData.customMetrics,
|
||||
meta: {
|
||||
queryRangeSecs: 900,
|
||||
queryOffsetSecs: 0,
|
||||
},
|
||||
},
|
||||
end: 'end::pageReady',
|
||||
start: 'start::pageChange',
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
const eventData = {
|
||||
meta: { rangeFrom: '2024-12-09T00:00:00Z', rangeTo: '2024-12-09T00:30:00Z' },
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
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
|
||||
|
||||
const eventData = {
|
||||
meta: { rangeFrom: '2024-12-08T00:00:00Z', rangeTo: '2024-12-09T00:00:00Z' },
|
||||
};
|
||||
|
||||
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: 86400,
|
||||
queryOffsetSecs: -1800,
|
||||
},
|
||||
},
|
||||
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
|
||||
|
||||
const eventData = {
|
||||
meta: { rangeFrom: '2024-12-08T01:00:00Z', rangeTo: '2024-12-09T01:00:00Z' },
|
||||
};
|
||||
|
||||
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: 86400,
|
||||
queryOffsetSecs: 1800,
|
||||
},
|
||||
},
|
||||
end: 'end::pageReady',
|
||||
start: 'start::pageChange',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not measure the same route twice', () => {
|
||||
const interaction = measureInteraction();
|
||||
const pathname = '/test-path';
|
||||
|
||||
interaction.pageReady(pathname);
|
||||
interaction.pageReady(pathname);
|
||||
|
||||
expect(performance.measure).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -10,38 +10,19 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import { afterFrame } from '@elastic/apm-rum-core';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { perfomanceMarkers } from '../performance_markers';
|
||||
import { PerformanceApi, PerformanceContext } from './use_performance_context';
|
||||
import { PerformanceMetricEvent } from '../../performance_metric_events';
|
||||
import { measureInteraction } from './measure_interaction';
|
||||
|
||||
export type CustomMetrics = Omit<PerformanceMetricEvent, 'eventName' | 'meta' | 'duration'>;
|
||||
|
||||
function measureInteraction() {
|
||||
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, customMetrics?: CustomMetrics) {
|
||||
performance.mark(perfomanceMarkers.endPageReady);
|
||||
|
||||
if (!trackedRoutes.includes(pathname)) {
|
||||
performance.measure(pathname, {
|
||||
detail: {
|
||||
eventName: 'kibana:plugin_render_time',
|
||||
type: 'kibana:performance',
|
||||
customMetrics,
|
||||
},
|
||||
start: perfomanceMarkers.startPageChange,
|
||||
end: perfomanceMarkers.endPageReady,
|
||||
});
|
||||
trackedRoutes.push(pathname);
|
||||
}
|
||||
},
|
||||
};
|
||||
export interface Meta {
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
}
|
||||
export interface EventData {
|
||||
customMetrics?: CustomMetrics;
|
||||
meta?: Meta;
|
||||
}
|
||||
|
||||
export function PerformanceContextProvider({ children }: { children: React.ReactElement }) {
|
||||
|
@ -61,9 +42,9 @@ export function PerformanceContextProvider({ children }: { children: React.React
|
|||
|
||||
const api = useMemo<PerformanceApi>(
|
||||
() => ({
|
||||
onPageReady(customMetrics) {
|
||||
onPageReady(eventData) {
|
||||
if (isRendered) {
|
||||
interaction.pageReady(location.pathname, customMetrics);
|
||||
interaction.pageReady(location.pathname, eventData);
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
|
|
@ -8,18 +8,22 @@
|
|||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CustomMetrics } from './performance_context';
|
||||
import type { CustomMetrics, Meta } from './performance_context';
|
||||
import { usePerformanceContext } from '../../..';
|
||||
|
||||
export const usePageReady = (state: { customMetrics?: CustomMetrics; isReady: boolean }) => {
|
||||
export const usePageReady = (state: {
|
||||
customMetrics?: CustomMetrics;
|
||||
isReady: boolean;
|
||||
meta?: Meta;
|
||||
}) => {
|
||||
const { onPageReady } = usePerformanceContext();
|
||||
|
||||
const [isReported, setIsReported] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.isReady && !isReported) {
|
||||
onPageReady(state.customMetrics);
|
||||
onPageReady({ customMetrics: state.customMetrics, meta: state.meta });
|
||||
setIsReported(true);
|
||||
}
|
||||
}, [isReported, onPageReady, state.customMetrics, state.isReady]);
|
||||
}, [isReported, onPageReady, state.customMetrics, state.isReady, state.meta]);
|
||||
};
|
||||
|
|
|
@ -8,14 +8,13 @@
|
|||
*/
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
import { CustomMetrics } from './performance_context';
|
||||
|
||||
import type { EventData } from './performance_context';
|
||||
export interface PerformanceApi {
|
||||
/**
|
||||
* 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 customMetrics - Custom metrics to be included in the performance measure.
|
||||
* @param eventData - Data to send with the performance measure, conforming the structure of a {@link EventData}.
|
||||
*/
|
||||
onPageReady(customMetrics?: CustomMetrics): void;
|
||||
onPageReady(eventData?: EventData): void;
|
||||
}
|
||||
|
||||
export const PerformanceContext = createContext<PerformanceApi | undefined>(undefined);
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"kbn_references": [
|
||||
"@kbn/logging-mocks"
|
||||
],
|
||||
"kbn_references": ["@kbn/logging-mocks", "@kbn/timerange"],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
28
packages/kbn-io-ts-utils/BUILD.bazel
Normal file
28
packages/kbn-io-ts-utils/BUILD.bazel
Normal file
|
@ -0,0 +1,28 @@
|
|||
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
|
||||
|
||||
SRCS = glob(
|
||||
[
|
||||
"**/*.ts",
|
||||
"**/*.js",
|
||||
],
|
||||
exclude = [
|
||||
"**/*.config.js",
|
||||
"**/*.mock.*",
|
||||
"**/*.test.*",
|
||||
"**/*.stories.*",
|
||||
"**/__snapshots__/**",
|
||||
"**/integration_tests/**",
|
||||
"**/mocks/**",
|
||||
"**/scripts/**",
|
||||
"**/storybook/**",
|
||||
"**/test_fixtures/**",
|
||||
"**/test_helpers/**",
|
||||
],
|
||||
)
|
||||
|
||||
js_library(
|
||||
name = "kbn-io-ts-utils",
|
||||
package_name = "@kbn/io-ts-utils",
|
||||
srcs = ["package.json"] + SRCS,
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
28
packages/kbn-timerange/BUILD.bazel
Normal file
28
packages/kbn-timerange/BUILD.bazel
Normal file
|
@ -0,0 +1,28 @@
|
|||
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
|
||||
|
||||
SRCS = glob(
|
||||
[
|
||||
"**/*.ts",
|
||||
"**/*.js",
|
||||
],
|
||||
exclude = [
|
||||
"**/*.config.js",
|
||||
"**/*.mock.*",
|
||||
"**/*.test.*",
|
||||
"**/*.stories.*",
|
||||
"**/__snapshots__/**",
|
||||
"**/integration_tests/**",
|
||||
"**/mocks/**",
|
||||
"**/scripts/**",
|
||||
"**/storybook/**",
|
||||
"**/test_fixtures/**",
|
||||
"**/test_helpers/**",
|
||||
],
|
||||
)
|
||||
|
||||
js_library(
|
||||
name = "kbn-timerange",
|
||||
package_name = "@kbn/timerange",
|
||||
srcs = ["package.json"] + SRCS,
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
|
@ -7,4 +7,9 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { getDateRange, getDateISORange } from './src';
|
||||
export {
|
||||
getDateRange,
|
||||
getDateISORange,
|
||||
getOffsetFromNowInSeconds,
|
||||
getTimeDifferenceInSeconds,
|
||||
} from './src';
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/timerange",
|
||||
"owner": [
|
||||
"@elastic/obs-ux-logs-team"
|
||||
],
|
||||
"group": "observability",
|
||||
"visibility": "private"
|
||||
}
|
||||
"owner": ["@elastic/obs-ux-logs-team"],
|
||||
"group": "platform",
|
||||
"visibility": "shared"
|
||||
}
|
||||
|
|
|
@ -52,3 +52,24 @@ export function getDateISORange({ from, to }: { from: string; to: string }) {
|
|||
endDate,
|
||||
};
|
||||
}
|
||||
|
||||
export function getTimeDifferenceInSeconds({
|
||||
startDate,
|
||||
endDate,
|
||||
}: {
|
||||
startDate: number;
|
||||
endDate: number;
|
||||
}): number {
|
||||
if (!startDate || !endDate || startDate > endDate) {
|
||||
throw new Error(`Invalid Dates: from: ${startDate}, to: ${endDate}`);
|
||||
}
|
||||
|
||||
const rangeInSeconds = (endDate - startDate) / 1000;
|
||||
return Math.round(rangeInSeconds);
|
||||
}
|
||||
|
||||
export function getOffsetFromNowInSeconds(epochDate: number) {
|
||||
const now = Date.now();
|
||||
|
||||
return Math.round((epochDate - now) / 1000);
|
||||
}
|
||||
|
|
|
@ -180,6 +180,9 @@ export function ServiceInventory() {
|
|||
const [renderedItems, setRenderedItems] = useState<ServiceListItem[]>([]);
|
||||
const mainStatisticsFetch = useServicesMainStatisticsFetcher(debouncedSearchQuery);
|
||||
const { mainStatisticsData, mainStatisticsStatus } = mainStatisticsFetch;
|
||||
const {
|
||||
query: { rangeFrom, rangeTo },
|
||||
} = useApmParams('/services');
|
||||
|
||||
const displayHealthStatus = mainStatisticsData.items.some((item) => 'healthStatus' in item);
|
||||
|
||||
|
@ -285,9 +288,11 @@ export function ServiceInventory() {
|
|||
mainStatisticsStatus === FETCH_STATUS.SUCCESS &&
|
||||
comparisonFetch.status === FETCH_STATUS.SUCCESS
|
||||
) {
|
||||
onPageReady();
|
||||
onPageReady({
|
||||
meta: { rangeFrom, rangeTo },
|
||||
});
|
||||
}
|
||||
}, [mainStatisticsStatus, comparisonFetch.status, onPageReady]);
|
||||
}, [mainStatisticsStatus, comparisonFetch.status, onPageReady, rangeFrom, rangeTo]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -186,7 +186,9 @@ export function ServiceMap({
|
|||
}
|
||||
|
||||
if (status === FETCH_STATUS.SUCCESS) {
|
||||
onPageReady();
|
||||
onPageReady({
|
||||
meta: { rangeFrom: start, rangeTo: end },
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -135,15 +135,20 @@ export function TraceList({ response }: Props) {
|
|||
const { data: { items } = { items: [] }, status } = response;
|
||||
const { onPageReady } = usePerformanceContext();
|
||||
|
||||
const { query } = useApmParams('/traces');
|
||||
const {
|
||||
query,
|
||||
query: { rangeFrom, rangeTo },
|
||||
} = useApmParams('/traces');
|
||||
|
||||
const traceListColumns = useMemo(() => getTraceListColumns({ query }), [query]);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === FETCH_STATUS.SUCCESS) {
|
||||
onPageReady();
|
||||
onPageReady({
|
||||
meta: { rangeFrom, rangeTo },
|
||||
});
|
||||
}
|
||||
}, [status, onPageReady]);
|
||||
}, [status, onPageReady, rangeFrom, rangeTo]);
|
||||
|
||||
return (
|
||||
<ManagedTable
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import React from 'react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiPanel,
|
||||
|
@ -30,9 +29,11 @@ import {
|
|||
summaryPanelQualityTooltipText,
|
||||
} from '../../../../common/translations';
|
||||
import { mapPercentagesToQualityCounts } from '../../quality_indicator';
|
||||
import { useDatasetQualityFilters } from '../../../hooks/use_dataset_quality_filters';
|
||||
|
||||
export function DatasetsQualityIndicators() {
|
||||
const { onPageReady } = usePerformanceContext();
|
||||
const { timeRange } = useDatasetQualityFilters();
|
||||
const {
|
||||
datasetsQuality,
|
||||
isDatasetsQualityLoading,
|
||||
|
@ -46,10 +47,16 @@ export function DatasetsQualityIndicators() {
|
|||
|
||||
if (!isDatasetsQualityLoading && (numberOfDatasets || numberOfDocuments)) {
|
||||
onPageReady({
|
||||
key1: 'datasets',
|
||||
value1: numberOfDatasets,
|
||||
key2: 'documents',
|
||||
value2: numberOfDocuments,
|
||||
customMetrics: {
|
||||
key1: 'datasets',
|
||||
value1: numberOfDatasets,
|
||||
key2: 'documents',
|
||||
value2: numberOfDocuments,
|
||||
},
|
||||
meta: {
|
||||
rangeFrom: timeRange.from,
|
||||
rangeTo: timeRange.to,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue