From db2efa064b7ecd572cf4428fe1333e2552d91771 Mon Sep 17 00:00:00 2001 From: Mykola Harmash Date: Tue, 18 Mar 2025 09:50:38 +0100 Subject: [PATCH 01/10] Merge onPageReady TTFMP performance event changes from upstream to keep things in sync. --- .../context/measure_interaction/index.ts | 68 ++++++++++++++----- .../context/performance_context.tsx | 18 +++-- .../public/application/app.tsx | 17 +++-- .../onboarding_flow_form.tsx | 8 +++ .../auto_detect/auto_detect_panel.tsx | 14 +++- .../quickstart_flows/firehose/index.tsx | 14 +++- .../quickstart_flows/kubernetes/index.tsx | 14 +++- .../otel_kubernetes/otel_kubernetes_panel.tsx | 14 +++- .../quickstart_flows/otel_logs/index.tsx | 12 ++++ .../observability_onboarding/tsconfig.json | 1 + 10 files changed, 146 insertions(+), 34 deletions(-) diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/index.ts b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/index.ts index 790314a8f0892..772ad3cc5dea4 100644 --- a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/index.ts +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/index.ts @@ -12,28 +12,31 @@ 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; + queryOffsetSecs?: 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 +45,57 @@ export function measureInteraction() { to: rangeTo, }); - performanceMeta = { - queryRangeSecs: getTimeDifferenceInSeconds(dateRangesInEpoch), - queryOffsetSecs: - rangeTo === 'now' ? 0 : getOffsetFromNowInSeconds(dateRangesInEpoch.endDate), - }; + performanceMeta.queryRangeSecs = getTimeDifferenceInSeconds(dateRangesInEpoch); + performanceMeta.queryOffsetSecs = + rangeTo === 'now' ? 0 : getOffsetFromNowInSeconds(dateRangesInEpoch.endDate); + } + + if (eventData?.meta?.description) { + performanceMeta.description = eventData.meta.description; } - 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); }, }; } diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/performance_context.tsx b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/performance_context.tsx index c3e60270c6ac4..51a485b6ede17 100644 --- a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/performance_context.tsx +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/performance_context.tsx @@ -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; 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 {children}; diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/app.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/app.tsx index c2163bda2f2da..b09ca846aee43 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/app.tsx +++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/app.tsx @@ -8,6 +8,7 @@ import { EuiErrorBoundary } from '@elastic/eui'; import { AppMountParameters, APP_WRAPPER_CLASS, CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; +import { PerformanceContextProvider } from '@kbn/ebt-tools'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; @@ -71,13 +72,15 @@ export function ObservabilityOnboardingAppRoot({ }} > - - - - + + + + + + diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx index e2308f91235c0..5ad766d61ced0 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx +++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx @@ -27,6 +27,7 @@ import { css } from '@emotion/react'; import { useSearchParams } from 'react-router-dom-v5-compat'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { IntegrationCardItem } from '@kbn/fleet-plugin/public'; +import { usePerformanceContext } from '@kbn/ebt-tools'; import { PackageListSearchForm } from '../package_list_search_form/package_list_search_form'; import { Category } from './types'; import { useCustomCards } from './use_custom_cards'; @@ -113,6 +114,7 @@ export const OnboardingFlowForm: FunctionComponent = () => { const radioGroupId = useGeneratedHtmlId({ prefix: 'onboardingCategory' }); const categorySelectorTitleId = useGeneratedHtmlId(); const packageListTitleId = useGeneratedHtmlId(); + const { onPageReady } = usePerformanceContext(); const [searchParams, setSearchParams] = useSearchParams(); @@ -148,6 +150,12 @@ export const OnboardingFlowForm: FunctionComponent = () => { [] // eslint-disable-line react-hooks/exhaustive-deps ); + useEffect(() => { + onPageReady({ + meta: { description: '[ttfmp_onboarding] The UI with onboarding categories is rendered' }, + }); + }, [onPageReady]); + const featuredCardsForCategoryMap: Record = { host: ['auto-detect-logs', 'otel-logs'], kubernetes: ['kubernetes-quick-start', 'otel-kubernetes'], diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/auto_detect/auto_detect_panel.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/auto_detect/auto_detect_panel.tsx index 5c4ca8eb29fe4..c8297d835fdf3 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/auto_detect/auto_detect_panel.tsx +++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/auto_detect/auto_detect_panel.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { type FunctionComponent } from 'react'; +import React, { useEffect, type FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiPanel, @@ -24,6 +24,7 @@ import { import { useKibana } from '@kbn/kibana-react-plugin/public'; import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; import { ASSET_DETAILS_LOCATOR_ID } from '@kbn/observability-shared-plugin/common'; +import { usePerformanceContext } from '@kbn/ebt-tools'; import { getAutoDetectCommand } from './get_auto_detect_command'; import { DASHBOARDS, useOnboardingFlow } from './use_onboarding_flow'; import { ProgressIndicator } from '../shared/progress_indicator'; @@ -41,10 +42,21 @@ export const AutoDetectPanel: FunctionComponent = () => { const { status, data, error, refetch, installedIntegrations } = useOnboardingFlow(); const command = data ? getAutoDetectCommand(data) : undefined; const accordionId = useGeneratedHtmlId({ prefix: 'accordion' }); + const { onPageReady } = usePerformanceContext(); const { services: { share }, } = useKibana(); + useEffect(() => { + if (data) { + onPageReady({ + meta: { + description: `[ttfmp_onboarding] Request to create the onboarding flow succeeded and the flow's UI has rendered`, + }, + }); + } + }, [data, onPageReady]); + if (error) { return ; } diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/firehose/index.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/firehose/index.tsx index 01e2dd02c3c47..05d16ea032420 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/firehose/index.tsx +++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/firehose/index.tsx @@ -18,8 +18,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { usePerformanceContext } from '@kbn/ebt-tools'; import { OnboardingFlowEventContext } from '../../../../common/telemetry_events'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { EmptyPrompt } from '../shared/empty_prompt'; @@ -64,6 +65,17 @@ export function FirehosePanel() { } = useKibana(); const { data, status, error, refetch } = useFirehoseFlow(); const { data: populatedAWSIndexList } = usePopulatedAWSIndexList(); + const { onPageReady } = usePerformanceContext(); + + useEffect(() => { + if (data) { + onPageReady({ + meta: { + description: `[ttfmp_onboarding] Request to create the onboarding flow succeeded and the flow's UI has rendered`, + }, + }); + } + }, [data, onPageReady]); const hasExistingData = Array.isArray(populatedAWSIndexList) && populatedAWSIndexList.length > 0; diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/kubernetes/index.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/kubernetes/index.tsx index 96b7b5619377b..fb094eefc608f 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/kubernetes/index.tsx +++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/kubernetes/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { EuiPanel, EuiSkeletonRectangle, @@ -15,6 +15,7 @@ import { EuiStepStatus, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { usePerformanceContext } from '@kbn/ebt-tools'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { EmptyPrompt } from '../shared/empty_prompt'; import { CommandSnippet } from './command_snippet'; @@ -25,6 +26,7 @@ import { useWindowBlurDataMonitoringTrigger } from '../shared/use_window_blur_da export const KubernetesPanel: React.FC = () => { const { data, status, error, refetch } = useKubernetesFlow(); + const { onPageReady } = usePerformanceContext(); const isMonitoringStepActive = useWindowBlurDataMonitoringTrigger({ isActive: status === FETCH_STATUS.SUCCESS, @@ -32,6 +34,16 @@ export const KubernetesPanel: React.FC = () => { onboardingId: data?.onboardingId, }); + useEffect(() => { + if (data) { + onPageReady({ + meta: { + description: `[ttfmp_onboarding] Request to create the onboarding flow succeeded and the flow's UI has rendered`, + }, + }); + } + }, [data, onPageReady]); + if (error !== undefined) { return ; } diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/otel_kubernetes/otel_kubernetes_panel.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/otel_kubernetes/otel_kubernetes_panel.tsx index 9e2d392a9c45c..aa8389c6f0ebd 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/otel_kubernetes/otel_kubernetes_panel.tsx +++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/otel_kubernetes/otel_kubernetes_panel.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiPanel, EuiSkeletonText, @@ -26,6 +26,7 @@ import { useKibana } from '@kbn/kibana-react-plugin/public'; import { FormattedMessage } from '@kbn/i18n-react'; import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; import { css } from '@emotion/react'; +import { usePerformanceContext } from '@kbn/ebt-tools'; import { EmptyPrompt } from '../shared/empty_prompt'; import { GetStartedPanel } from '../shared/get_started_panel'; import { FeedbackButtons } from '../shared/feedback_buttons'; @@ -46,6 +47,17 @@ export const OtelKubernetesPanel: React.FC = () => { const apmLocator = share.url.locators.get('APM_LOCATOR'); const dashboardLocator = share.url.locators.get(DASHBOARD_APP_LOCATOR); const theme = useEuiTheme(); + const { onPageReady } = usePerformanceContext(); + + useEffect(() => { + if (data) { + onPageReady({ + meta: { + description: `[ttfmp_onboarding] Request to create the onboarding flow succeeded and the flow's UI has rendered`, + }, + }); + } + }, [data, onPageReady]); if (error) { return ( diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/otel_logs/index.tsx b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/otel_logs/index.tsx index fe1f68164dbc5..751431f1ca542 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/otel_logs/index.tsx +++ b/x-pack/solutions/observability/plugins/observability_onboarding/public/application/quickstart_flows/otel_logs/index.tsx @@ -29,6 +29,7 @@ import { i18n } from '@kbn/i18n'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import useAsyncFn from 'react-use/lib/useAsyncFn'; import { FormattedMessage } from '@kbn/i18n-react'; +import { usePerformanceContext } from '@kbn/ebt-tools'; import { ObservabilityOnboardingAppServices } from '../../..'; import { useFetcher } from '../../../hooks/use_fetcher'; import { MultiIntegrationInstallBanner } from './multi_integration_install_banner'; @@ -44,6 +45,7 @@ const HOST_COMMAND = i18n.translate( ); export const OtelLogsPanel: React.FC = () => { + const { onPageReady } = usePerformanceContext(); const { data: apiKeyData, error, @@ -68,6 +70,16 @@ export const OtelLogsPanel: React.FC = () => { }, } = useKibana(); + useEffect(() => { + if (apiKeyData && setup) { + onPageReady({ + meta: { + description: `[ttfmp_onboarding] Requests to get the environment and to generate API key succeeded and the flow's UI has rendered`, + }, + }); + } + }, [apiKeyData, onPageReady, setup]); + const AGENT_CDN_BASE_URL = 'artifacts.elastic.co/downloads/beats/elastic-agent'; const agentVersion = isServerless && setup ? setup.elasticAgentVersionInfo.agentVersion : stackVersion; diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/tsconfig.json b/x-pack/solutions/observability/plugins/observability_onboarding/tsconfig.json index cd52f8285fa07..344cd996fbeb3 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/tsconfig.json +++ b/x-pack/solutions/observability/plugins/observability_onboarding/tsconfig.json @@ -46,6 +46,7 @@ "@kbn/server-route-repository-utils", "@kbn/core-application-browser", "@kbn/core-plugins-server", + "@kbn/ebt-tools" ], "exclude": [ "target/**/*" From 9e94b89b0c3175ae0bcd43357f58b6985008749d Mon Sep 17 00:00:00 2001 From: Abdul Zahid Date: Thu, 3 Apr 2025 23:39:42 +0200 Subject: [PATCH 02/10] Import `types.ts` from `main`. --- .../src/performance_metrics/context/types.ts | 16 ++++++++++++++++ .../context/use_performance_context.tsx | 13 +++++++++++++ .../performance_metrics/performance_markers.ts | 1 + 3 files changed, 30 insertions(+) create mode 100644 src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/types.ts diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/types.ts b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/types.ts new file mode 100644 index 0000000000000..5591880fd2ac0 --- /dev/null +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/types.ts @@ -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}`; diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/use_performance_context.tsx b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/use_performance_context.tsx index a2fab435778e1..68d724b7bf7cb 100644 --- a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/use_performance_context.tsx +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/use_performance_context.tsx @@ -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(undefined); diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/performance_markers.ts b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/performance_markers.ts index 75a807e33b5c8..f5d9aef03135e 100644 --- a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/performance_markers.ts +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/performance_markers.ts @@ -11,4 +11,5 @@ export const perfomanceMarkers = { startPageChange: 'start::pageChange', endPageReady: 'end::pageReady', + startPageRefresh: 'start::pageRefresh', }; From 4ee971e760fc852d123bfd996eb7adc093b937ac Mon Sep 17 00:00:00 2001 From: Abdul Zahid Date: Fri, 4 Apr 2025 20:26:53 +0200 Subject: [PATCH 03/10] Update search bar test. --- .../shared/search_bar/search_bar.test.tsx | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/x-pack/solutions/observability/plugins/apm/public/components/shared/search_bar/search_bar.test.tsx b/x-pack/solutions/observability/plugins/apm/public/components/shared/search_bar/search_bar.test.tsx index 04f1b4f39352f..6dd4063e64452 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/shared/search_bar/search_bar.test.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/shared/search_bar/search_bar.test.tsx @@ -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({ - + + + @@ -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 () => { From 0dc88165e94eec9b732d39c7038c9843059ae7a1 Mon Sep 17 00:00:00 2001 From: Abdul Zahid Date: Fri, 4 Apr 2025 08:48:52 +0200 Subject: [PATCH 04/10] Revert e2e inclusion. --- .../plugins/observability_onboarding/tsconfig.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/tsconfig.json b/x-pack/solutions/observability/plugins/observability_onboarding/tsconfig.json index 344cd996fbeb3..e5e5ef8d7a854 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/tsconfig.json +++ b/x-pack/solutions/observability/plugins/observability_onboarding/tsconfig.json @@ -9,8 +9,7 @@ "public/**/*", "typings/**/*", "public/**/*.json", - "server/**/*", - "e2e/**/*" + "server/**/*" ], "kbn_references": [ "@kbn/core", From d824f6048d21a16f99d95d4d0be155debe50c7f9 Mon Sep 17 00:00:00 2001 From: Abdul Zahid Date: Fri, 4 Apr 2025 20:51:53 +0200 Subject: [PATCH 05/10] Update unified_search_bar test. --- .../apm/public/context/apm_plugin/mock_apm_plugin_context.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/solutions/observability/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/solutions/observability/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index 6e961e14eaf64..af5c1bbbfe08f 100644 --- a/x-pack/solutions/observability/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -218,6 +218,9 @@ export function MockApmPluginContextWrapper({ createCallApmApi(contextValue.core); } + performance.mark = jest.fn(); + performance.clearMeasures = jest.fn(); + const contextHistory = useHistory(); const usedHistory = useMemo(() => { From a4054774c7ad89c350b5117de983a4bc549b03ab Mon Sep 17 00:00:00 2001 From: Abdul Zahid Date: Fri, 4 Apr 2025 21:32:25 +0200 Subject: [PATCH 06/10] Revert e2e inclusion. --- .../plugins/observability_onboarding/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/observability/plugins/observability_onboarding/tsconfig.json b/x-pack/solutions/observability/plugins/observability_onboarding/tsconfig.json index e5e5ef8d7a854..344cd996fbeb3 100644 --- a/x-pack/solutions/observability/plugins/observability_onboarding/tsconfig.json +++ b/x-pack/solutions/observability/plugins/observability_onboarding/tsconfig.json @@ -9,7 +9,8 @@ "public/**/*", "typings/**/*", "public/**/*.json", - "server/**/*" + "server/**/*", + "e2e/**/*" ], "kbn_references": [ "@kbn/core", From 7a4ff9432c2182896480d599264c0add8727ed83 Mon Sep 17 00:00:00 2001 From: Abdul Zahid Date: Fri, 4 Apr 2025 21:47:31 +0200 Subject: [PATCH 07/10] Update TTFMP refactor docs. --- .../performance/adding_custom_performance_metrics.mdx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dev_docs/tutorials/performance/adding_custom_performance_metrics.mdx b/dev_docs/tutorials/performance/adding_custom_performance_metrics.mdx index 7ebd918f4a331..b68e91b3fc6ca 100644 --- a/dev_docs/tutorials/performance/adding_custom_performance_metrics.mdx +++ b/dev_docs/tutorials/performance/adding_custom_performance_metrics.mdx @@ -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", From 086adf7b9296e8723959805f3effa21b047078b0 Mon Sep 17 00:00:00 2001 From: Abdul Zahid Date: Sun, 6 Apr 2025 02:11:29 +0200 Subject: [PATCH 08/10] Sync `kbn-ebt-tools/src/performance_metrics/context/measure_interaction` from `main`. --- .../context/measure_interaction/index.ts | 7 +- .../measure_interaction.test.tsx | 304 +++++++++++------- 2 files changed, 184 insertions(+), 127 deletions(-) diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/index.ts b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/index.ts index 772ad3cc5dea4..26f43aa7e66f8 100644 --- a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/index.ts +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/index.ts @@ -18,7 +18,8 @@ import { DescriptionWithPrefix } from '../types'; interface PerformanceMeta { queryRangeSecs?: number; - queryOffsetSecs?: number; + queryFromOffsetSecs?: number; + queryToOffsetSecs?: number; isInitialLoad?: boolean; description?: DescriptionWithPrefix; } @@ -46,7 +47,9 @@ export function measureInteraction(pathname: string) { }); performanceMeta.queryRangeSecs = getTimeDifferenceInSeconds(dateRangesInEpoch); - performanceMeta.queryOffsetSecs = + performanceMeta.queryFromOffsetSecs = + rangeFrom === 'now' ? 0 : getOffsetFromNowInSeconds(dateRangesInEpoch.startDate); + performanceMeta.queryToOffsetSecs = rangeTo === 'now' ? 0 : getOffsetFromNowInSeconds(dateRangesInEpoch.endDate); } diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/measure_interaction.test.tsx b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/measure_interaction.test.tsx index 5768a6126c571..fb8a3f3a0f8c7 100644 --- a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/measure_interaction.test.tsx +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/measure_interaction.test.tsx @@ -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(); + + performance.getEntriesByName = jest + .fn() + .mockReturnValueOnce([{ name: 'start::pageChange' }]) + .mockReturnValueOnce([{ name: 'end::pageReady' }]) + .mockReturnValueOnce([]); + performance.clearMarks = jest.fn(); + }); - it('should mark the start of the page change', () => { - measureInteraction(); - expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.startPageChange); - }); + it('should mark the start of the page change', () => { + const pathname = '/test-path'; + measureInteraction(pathname); + 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 mark the end of the page ready state and measure performance', () => { + const pathname = '/test-path'; + const interaction = measureInteraction(pathname); + + 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, + }, + }, + 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, + 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' }, + }; + + 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: 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 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, + 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-09T00:00:00Z', rangeTo: '2024-12-09T00:30: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: 1800, + queryFromOffsetSecs: -1800, + 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 - - 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, + 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-08T00:00:00Z', rangeTo: '2024-12-09T00: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: -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 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, + 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', + 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'; - - interaction.pageReady(pathname); - interaction.pageReady(pathname); - - expect(performance.measure).toHaveBeenCalledTimes(1); + 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); + + 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: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', + }); + }); }); }); From 9ebf59a0cb50f7440e1b24945991ce917c19d24c Mon Sep 17 00:00:00 2001 From: Abdul Zahid Date: Sun, 6 Apr 2025 20:57:35 +0200 Subject: [PATCH 09/10] Sync `kbn-ebt-tools` from `main`. --- .../context/measure_interaction/index.ts | 71 +--- .../measure_interaction.test.tsx | 304 +++++++----------- .../context/performance_context.tsx | 18 +- .../src/performance_metrics/context/types.ts | 16 - .../context/use_performance_context.tsx | 13 - .../performance_markers.ts | 1 - 6 files changed, 148 insertions(+), 275 deletions(-) delete mode 100644 src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/types.ts diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/index.ts b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/index.ts index 26f43aa7e66f8..790314a8f0892 100644 --- a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/index.ts +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/index.ts @@ -12,32 +12,28 @@ import { getOffsetFromNowInSeconds, getTimeDifferenceInSeconds, } from '@kbn/timerange'; -import { EventData } from '../performance_context'; import { perfomanceMarkers } from '../../performance_markers'; -import { DescriptionWithPrefix } from '../types'; +import { EventData } from '../performance_context'; interface PerformanceMeta { - queryRangeSecs?: number; - queryFromOffsetSecs?: number; - queryToOffsetSecs?: number; - isInitialLoad?: boolean; - description?: DescriptionWithPrefix; + queryRangeSecs: number; + queryOffsetSecs: number; } -export function measureInteraction(pathname: string) { +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(eventData?: EventData) { - const performanceMeta: PerformanceMeta = {}; + pageReady(pathname: string, eventData?: EventData) { + let performanceMeta: PerformanceMeta | undefined; performance.mark(perfomanceMarkers.endPageReady); - if (eventData?.meta?.rangeFrom && eventData?.meta?.rangeTo) { + if (eventData?.meta) { const { rangeFrom, rangeTo } = eventData.meta; // Convert the date range to epoch timestamps (in milliseconds) @@ -46,59 +42,26 @@ export function measureInteraction(pathname: string) { to: rangeTo, }); - performanceMeta.queryRangeSecs = getTimeDifferenceInSeconds(dateRangesInEpoch); - performanceMeta.queryFromOffsetSecs = - rangeFrom === 'now' ? 0 : getOffsetFromNowInSeconds(dateRangesInEpoch.startDate); - performanceMeta.queryToOffsetSecs = - rangeTo === 'now' ? 0 : getOffsetFromNowInSeconds(dateRangesInEpoch.endDate); - } - - if (eventData?.meta?.description) { - performanceMeta.description = eventData.meta.description; + performanceMeta = { + queryRangeSecs: getTimeDifferenceInSeconds(dateRangesInEpoch), + queryOffsetSecs: + rangeTo === 'now' ? 0 : getOffsetFromNowInSeconds(dateRangesInEpoch.endDate), + }; } - if ( - performance.getEntriesByName(perfomanceMarkers.startPageChange).length > 0 && - performance.getEntriesByName(perfomanceMarkers.endPageReady).length > 0 - ) { - performance.measure(`[ttfmp:initial] - ${pathname}`, { + if (!trackedRoutes.includes(pathname)) { + performance.measure(pathname, { detail: { eventName: 'kibana:plugin_render_time', type: 'kibana:performance', customMetrics: eventData?.customMetrics, - meta: { ...performanceMeta, isInitialLoad: true }, + meta: performanceMeta, }, start: perfomanceMarkers.startPageChange, end: perfomanceMarkers.endPageReady, }); - - // Clean up the marks once the measure is done - performance.clearMarks(perfomanceMarkers.startPageChange); - performance.clearMarks(perfomanceMarkers.endPageReady); + trackedRoutes.push(pathname); } - - 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); }, }; } diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/measure_interaction.test.tsx b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/measure_interaction.test.tsx index fb8a3f3a0f8c7..5768a6126c571 100644 --- a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/measure_interaction.test.tsx +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/measure_interaction.test.tsx @@ -15,201 +15,147 @@ describe('measureInteraction', () => { jest.restoreAllMocks(); }); - describe('Initial load', () => { - beforeEach(() => { - jest.clearAllMocks(); - performance.mark = jest.fn(); - performance.measure = jest.fn(); - - performance.getEntriesByName = jest - .fn() - .mockReturnValueOnce([{ name: 'start::pageChange' }]) - .mockReturnValueOnce([{ name: 'end::pageReady' }]) - .mockReturnValueOnce([]); - performance.clearMarks = jest.fn(); - }); + beforeEach(() => { + jest.clearAllMocks(); + performance.mark = jest.fn(); + performance.measure = jest.fn(); + }); - it('should mark the start of the page change', () => { - const pathname = '/test-path'; - measureInteraction(pathname); - expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.startPageChange); - }); + 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 pathname = '/test-path'; - const interaction = measureInteraction(pathname); - - 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, - }, - }, - start: perfomanceMarkers.startPageChange, - end: perfomanceMarkers.endPageReady, - }); + 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 pathname = '/test-path'; - const interaction = measureInteraction(pathname); - const eventData = { - customMetrics: { key1: 'foo-metric', value1: 100 }, - meta: { rangeFrom: 'now-15m', rangeTo: 'now' }, - }; - - 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: eventData.customMetrics, - meta: { - queryRangeSecs: 900, - queryFromOffsetSecs: -900, - queryToOffsetSecs: 0, - isInitialLoad: true, - }, + 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', - }); + }, + end: 'end::pageReady', + start: 'start::pageChange', }); + }); - 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-09T00:00:00Z', rangeTo: '2024-12-09T00:30: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: 1800, - queryFromOffsetSecs: -1800, - queryToOffsetSecs: 0, - isInitialLoad: true, - }, + 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', - }); + }, + end: 'end::pageReady', + start: 'start::pageChange', }); + }); - 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-08T00:00:00Z', rangeTo: '2024-12-09T00: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: -88200, - queryToOffsetSecs: -1800, - isInitialLoad: true, - }, + 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', - }); + }, + 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, - }, + 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', - }); - expect(performance.clearMarks).toHaveBeenCalledWith(perfomanceMarkers.startPageChange); - expect(performance.clearMarks).toHaveBeenCalledWith(perfomanceMarkers.endPageReady); + }, + end: 'end::pageReady', + start: 'start::pageChange', }); }); - 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); - - 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: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', - }); - }); + 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); }); }); diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/performance_context.tsx b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/performance_context.tsx index 51a485b6ede17..c3e60270c6ac4 100644 --- a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/performance_context.tsx +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/performance_context.tsx @@ -13,15 +13,13 @@ 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; export interface Meta { - rangeFrom?: string; - rangeTo?: string; - description?: DescriptionWithPrefix; + rangeFrom: string; + rangeTo: string; } - export interface EventData { customMetrics?: CustomMetrics; meta?: Meta; @@ -30,8 +28,7 @@ export interface EventData { export function PerformanceContextProvider({ children }: { children: React.ReactElement }) { const [isRendered, setIsRendered] = useState(false); const location = useLocation(); - - const interaction = useMemo(() => measureInteraction(location.pathname), [location.pathname]); + const interaction = measureInteraction(); React.useEffect(() => { afterFrame(() => { @@ -47,14 +44,11 @@ export function PerformanceContextProvider({ children }: { children: React.React () => ({ onPageReady(eventData) { if (isRendered) { - interaction.pageReady(eventData); + interaction.pageReady(location.pathname, eventData); } }, - onPageRefreshStart() { - interaction.pageRefreshStart(); - }, }), - [isRendered, interaction] + [isRendered, location.pathname, interaction] ); return {children}; diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/types.ts b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/types.ts deleted file mode 100644 index 5591880fd2ac0..0000000000000 --- a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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}`; diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/use_performance_context.tsx b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/use_performance_context.tsx index 68d724b7bf7cb..a2fab435778e1 100644 --- a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/use_performance_context.tsx +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/use_performance_context.tsx @@ -15,19 +15,6 @@ 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(undefined); diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/performance_markers.ts b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/performance_markers.ts index f5d9aef03135e..75a807e33b5c8 100644 --- a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/performance_markers.ts +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/performance_markers.ts @@ -11,5 +11,4 @@ export const perfomanceMarkers = { startPageChange: 'start::pageChange', endPageReady: 'end::pageReady', - startPageRefresh: 'start::pageRefresh', }; From e086752c6bc9e2d5d61c4c77e59d9fbe70a91555 Mon Sep 17 00:00:00 2001 From: Abdul Zahid Date: Mon, 7 Apr 2025 00:11:21 +0200 Subject: [PATCH 10/10] Sync `kbn-ebt-tools` from `main`. --- .../context/measure_interaction/index.ts | 71 +++- .../measure_interaction.test.tsx | 304 +++++++++++------- .../context/performance_context.tsx | 18 +- .../src/performance_metrics/context/types.ts | 16 + .../context/use_performance_context.tsx | 13 + .../performance_markers.ts | 1 + 6 files changed, 275 insertions(+), 148 deletions(-) create mode 100644 src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/types.ts diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/index.ts b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/index.ts index 790314a8f0892..26f43aa7e66f8 100644 --- a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/index.ts +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/index.ts @@ -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 (eventData?.meta?.description) { + performanceMeta.description = eventData.meta.description; } - 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); }, }; } diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/measure_interaction.test.tsx b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/measure_interaction.test.tsx index 5768a6126c571..fb8a3f3a0f8c7 100644 --- a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/measure_interaction.test.tsx +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/measure_interaction/measure_interaction.test.tsx @@ -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(); + + performance.getEntriesByName = jest + .fn() + .mockReturnValueOnce([{ name: 'start::pageChange' }]) + .mockReturnValueOnce([{ name: 'end::pageReady' }]) + .mockReturnValueOnce([]); + performance.clearMarks = jest.fn(); + }); - it('should mark the start of the page change', () => { - measureInteraction(); - expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.startPageChange); - }); + it('should mark the start of the page change', () => { + const pathname = '/test-path'; + measureInteraction(pathname); + 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 mark the end of the page ready state and measure performance', () => { + const pathname = '/test-path'; + const interaction = measureInteraction(pathname); + + 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, + }, + }, + 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, + 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' }, + }; + + 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: 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 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, + 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-09T00:00:00Z', rangeTo: '2024-12-09T00:30: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: 1800, + queryFromOffsetSecs: -1800, + 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 - - 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, + 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-08T00:00:00Z', rangeTo: '2024-12-09T00: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: -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 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, + 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', + 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'; - - interaction.pageReady(pathname); - interaction.pageReady(pathname); - - expect(performance.measure).toHaveBeenCalledTimes(1); + 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); + + 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: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', + }); + }); }); }); diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/performance_context.tsx b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/performance_context.tsx index c3e60270c6ac4..51a485b6ede17 100644 --- a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/performance_context.tsx +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/performance_context.tsx @@ -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; 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 {children}; diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/types.ts b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/types.ts new file mode 100644 index 0000000000000..5591880fd2ac0 --- /dev/null +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/types.ts @@ -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}`; diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/use_performance_context.tsx b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/use_performance_context.tsx index a2fab435778e1..68d724b7bf7cb 100644 --- a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/use_performance_context.tsx +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/context/use_performance_context.tsx @@ -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(undefined); diff --git a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/performance_markers.ts b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/performance_markers.ts index 75a807e33b5c8..f5d9aef03135e 100644 --- a/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/performance_markers.ts +++ b/src/platform/packages/shared/kbn-ebt-tools/src/performance_metrics/performance_markers.ts @@ -11,4 +11,5 @@ export const perfomanceMarkers = { startPageChange: 'start::pageChange', endPageReady: 'end::pageReady', + startPageRefresh: 'start::pageRefresh', };