Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Perfomance] Track time range picker with onPageReady function #202889

Merged
merged 21 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-ebt-tools/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ SHARED_DEPS = [
"@npm//@elastic/apm-rum-core",
"@npm//react",
"@npm//react-router-dom",
"//packages/kbn-timerange"
]

js_library(
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* 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', () => {
const mockDate = new Date('2024-12-09T15:00:00Z').getTime(); // Hardcoded date (Jan 1, 2024)

afterAll(() => {
// Restore the original Date.now() implementation after the tests
jest.restoreAllMocks();
jest.spyOn(Date, 'now').mockReturnValue(mockDate);
});

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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand All @@ -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);
}
},
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 1 addition & 3 deletions packages/kbn-ebt-tools/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
"types": ["jest", "node"]
},
"include": ["**/*.ts", "**/*.tsx"],
"kbn_references": [
"@kbn/logging-mocks"
],
"kbn_references": ["@kbn/logging-mocks", "@kbn/timerange"],
"exclude": ["target/**/*"]
}
Loading
Loading