[Perfomance] Add Inline documentation for TTFMP (#212393)

## Summary

closes https://github.com/elastic/observability-dev/issues/4101 

<img width="1728" alt="image"
src="https://github.com/user-attachments/assets/4937722f-f05b-404b-9844-930e80c8e15e"
/>


### ⚠️ Instrumentation

Pass the `description` as metadata. The prefix [TTFMP] is required. 

### How to test

- Checkout the PR
- make sure you run `yarn kbn bootstrap`
- go to any page that has onPageReady function instrumented (ex
services)
This commit is contained in:
Katerina 2025-03-04 16:33:38 +02:00 committed by GitHub
parent f74b6b52dc
commit a16dc711fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 110 additions and 3 deletions

View file

@ -157,7 +157,81 @@ describe('trackPerformanceMeasureEntries', () => {
expect(analyticsClientMock.reportEvent).toHaveBeenCalledWith('performance_metric', {
duration: 1000,
eventName: 'kibana:plugin_render_time',
meta: { query_range_secs: 86400, query_offset_secs: 0 },
meta: {
query_range_secs: 86400,
query_offset_secs: 0,
},
});
});
test('reports an analytics event with description metadata', () => {
setupMockPerformanceObserver([
{
name: '/',
entryType: 'measure',
startTime: 100,
duration: 1000,
detail: {
eventName: 'kibana:plugin_render_time',
type: 'kibana:performance',
meta: {
isInitialLoad: false,
description:
'[ttfmp_dependencies] onPageReady is called when the most important content is rendered',
},
},
},
]);
trackPerformanceMeasureEntries(analyticsClientMock, true);
expect(analyticsClientMock.reportEvent).toHaveBeenCalledTimes(1);
expect(analyticsClientMock.reportEvent).toHaveBeenCalledWith('performance_metric', {
duration: 1000,
eventName: 'kibana:plugin_render_time',
meta: {
is_initial_load: false,
query_range_secs: undefined,
query_offset_secs: undefined,
description:
'[ttfmp_dependencies] onPageReady is called when the most important content is rendered',
},
});
});
test('reports an analytics event with truncated description metadata', () => {
setupMockPerformanceObserver([
{
name: '/',
entryType: 'measure',
startTime: 100,
duration: 1000,
detail: {
eventName: 'kibana:plugin_render_time',
type: 'kibana:performance',
meta: {
isInitialLoad: false,
description:
'[ttfmp_dependencies] This is a very long long long long long long long long description. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque non risus in nunc tincidunt tincidunt. Proin vehicula, nunc at feugiat cursus, justo nulla fermentum lorem, non ultricies metus libero nec purus. Sed ut perspiciatis unde omnis iste natus.',
},
},
},
]);
trackPerformanceMeasureEntries(analyticsClientMock, true);
const truncatedDescription =
'[ttfmp_dependencies] This is a very long long long long long long long long description. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque non risus in nunc tincidunt tincidunt. Proin vehicula, nunc at feugiat cursus, justo nulla fermentum l';
expect(analyticsClientMock.reportEvent).toHaveBeenCalledTimes(1);
expect(analyticsClientMock.reportEvent).toHaveBeenCalledWith('performance_metric', {
duration: 1000,
eventName: 'kibana:plugin_render_time',
meta: {
is_initial_load: false,
query_range_secs: undefined,
query_offset_secs: undefined,
description: truncatedDescription,
},
});
expect(truncatedDescription.length).toBe(256);
});
});

View file

@ -11,6 +11,7 @@ import type { AnalyticsClient } from '@elastic/ebt/client';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
const MAX_CUSTOM_METRICS = 9;
const MAX_DESCRIPTION_LENGTH = 256;
// The keys and values for the custom metrics are limited to 9 pairs
const ALLOWED_CUSTOM_METRICS_KEYS_VALUES = Array.from({ length: MAX_CUSTOM_METRICS }, (_, i) => [
`key${i + 1}`,
@ -28,6 +29,8 @@ export function trackPerformanceMeasureEntries(analytics: AnalyticsClient, isDev
const target = entry?.name;
const duration = entry.duration;
const meta = entry.detail?.meta;
const description = meta?.description;
const customMetrics = Object.keys(entry.detail?.customMetrics ?? {}).reduce(
(acc, metric) => {
if (ALLOWED_CUSTOM_METRICS_KEYS_VALUES.includes(metric)) {
@ -55,6 +58,13 @@ export function trackPerformanceMeasureEntries(analytics: AnalyticsClient, isDev
);
}
if (description?.length > MAX_DESCRIPTION_LENGTH) {
// eslint-disable-next-line no-console
console.warn(
`The description for the measure: ${target} is too long. The maximum length is ${MAX_DESCRIPTION_LENGTH}. Strings longer than ${MAX_DESCRIPTION_LENGTH} will not be indexed or stored`
);
}
// eslint-disable-next-line no-console
console.log(`The measure ${target} completed in ${duration / 1000}s`);
}
@ -74,6 +84,7 @@ export function trackPerformanceMeasureEntries(analytics: AnalyticsClient, isDev
meta: {
query_range_secs: meta?.queryRangeSecs,
query_offset_secs: meta?.queryOffsetSecs,
description: description?.slice(0, MAX_DESCRIPTION_LENGTH),
is_initial_load: meta?.isInitialLoad,
},
});

View file

@ -14,11 +14,13 @@ import {
} from '@kbn/timerange';
import { EventData } from '../performance_context';
import { perfomanceMarkers } from '../../performance_markers';
import { DescriptionWithPrefix } from '../types';
interface PerformanceMeta {
queryRangeSecs: number;
queryOffsetSecs: number;
isInitialLoad?: boolean;
description?: DescriptionWithPrefix;
}
export function measureInteraction(pathname: string) {
@ -35,7 +37,7 @@ export function measureInteraction(pathname: string) {
performance.mark(perfomanceMarkers.endPageReady);
if (eventData?.meta) {
const { rangeFrom, rangeTo } = eventData.meta;
const { rangeFrom, rangeTo, description } = eventData.meta;
// Convert the date range to epoch timestamps (in milliseconds)
const dateRangesInEpoch = getDateRange({
@ -47,6 +49,7 @@ export function measureInteraction(pathname: string) {
queryRangeSecs: getTimeDifferenceInSeconds(dateRangesInEpoch),
queryOffsetSecs:
rangeTo === 'now' ? 0 : getOffsetFromNowInSeconds(dateRangesInEpoch.endDate),
description,
};
}

View file

@ -13,13 +13,15 @@ import { useLocation } from 'react-router-dom';
import { PerformanceApi, PerformanceContext } from './use_performance_context';
import { PerformanceMetricEvent } from '../../performance_metric_events';
import { measureInteraction } from './measure_interaction';
import { DescriptionWithPrefix } from './types';
export type CustomMetrics = Omit<PerformanceMetricEvent, 'eventName' | 'meta' | 'duration'>;
export interface Meta {
rangeFrom: string;
rangeTo: string;
description?: DescriptionWithPrefix;
}
export interface EventData {
customMetrics?: CustomMetrics;
meta?: Meta;

View file

@ -0,0 +1,15 @@
/*
* 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';
export type Key = `${ApmPageId}` | `${InfraPageId}`;
export type DescriptionWithPrefix = `[ttfmp_${Key}] ${string}`;

View file

@ -59,6 +59,8 @@ export function DependenciesInventoryTable() {
meta: {
rangeFrom,
rangeTo,
description:
'[ttfmp_dependencies] Dependencies table is ready after fetching top_dependencies.',
},
});
}