Skip to content

Commit

Permalink
[Perfomance] Track time range picker with onPageReady function (ela…
Browse files Browse the repository at this point in the history
…stic#202889)

## Summary

closes elastic/observability-dev#3377
## Metrics 
#### `meta.query_range_secs` - The duration of the selected time range
in seconds.
#### `meta.query_offset_secs` - The offset from "now" to the
'rangeTo'/end' time picker value in seconds.

____

Extend the `onPageReady` function to support date ranges in the meta
field. The function should compute the query range in seconds based on
the provided time range and report it to telemetry as
meta.query_range_secs.




If the `rangeTo` is different from 'now', calculate the offset. 
- A negative offset indicates that the rangeTo is in the past, 
- a positive offset means it is in the future, 
- and zero indicates that the rangeTo is exactly 'now'." 



### How to instrument
To report the selected time range, pass the `rangeFrom` and `rangeTo` . 
> Failing to pass the correct type will result in TS error.


Then, use this data when invoking onPageReady:
```
 onPageReady({
        meta: { rangeFrom, rangeTo },
 });
```

### Analysis 

Meta is flatten field. In order to aggregate the data it's necessary to
create a run time field. You can add a field in the

1. select data view (`ebt-kibana-*-performance-metrics`) 
2. Add a new field
3. Type double
4. Set value 

`query_range_secs`
```
def meta = doc[“meta”].size();
if (meta > 0) {
    def range = doc[“meta.query_range_secs”].size();
    if (range > 0) {
        // Emit the value of ‘meta.target’
        emit(Double.parseDouble(doc[“meta.query_range_secs”].value));
    }
}

```

`query_offset_secs` 
```

def meta = doc[“meta”].size();
if (meta > 0) {
    def offset = doc[“meta.query_offset_secs”].size();
    if (offset > 0) {
     
        emit(Double.parseDouble(doc[“meta.query_offset_secs”].value));
    }
}

```







### Examples


<img width="1478" alt="Screenshot 2024-12-09 at 19 51 32"
src="https://github.com/user-attachments/assets/72f796e1-4f20-487f-b62a-b6a4aead9a4a">

<img width="1478" alt="Screenshot 2024-12-09 at 19 56 08"
src="https://github.com/user-attachments/assets/c278dc3b-e6f3-47ed-9c90-954d71b59161">

<img width="1478" alt="Screenshot 2024-12-09 at 19 53 45 1"
src="https://github.com/user-attachments/assets/ef42ecef-48cd-4396-9f5d-c971098d5219">





### Notes
- Instrumented only 2 solutions as an example (dataset and apm services)

### TODO
- [x] Update documentation -
elastic#204179
- [ ] Update dashboards (create a runtime field) 
- [x] Track offset ( we need to know if the user selected now or now)

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
kpatticha and kibanamachine authored Dec 13, 2024
1 parent 492d4d2 commit bd1c00f
Show file tree
Hide file tree
Showing 18 changed files with 398 additions and 58 deletions.
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,161 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { measureInteraction } from '.';
import { perfomanceMarkers } from '../../performance_markers';

describe('measureInteraction', () => {
afterAll(() => {
jest.restoreAllMocks();
});

beforeEach(() => {
jest.clearAllMocks();
performance.mark = jest.fn();
performance.measure = jest.fn();
});

it('should mark the start of the page change', () => {
measureInteraction();
expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.startPageChange);
});

it('should mark the end of the page ready state and measure performance', () => {
const interaction = measureInteraction();
const pathname = '/test-path';
interaction.pageReady(pathname);

expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.endPageReady);
expect(performance.measure).toHaveBeenCalledWith(pathname, {
detail: {
eventName: 'kibana:plugin_render_time',
type: 'kibana:performance',
},
start: perfomanceMarkers.startPageChange,
end: perfomanceMarkers.endPageReady,
});
});

it('should include custom metrics and meta in the performance measure', () => {
const interaction = measureInteraction();
const pathname = '/test-path';
const eventData = {
customMetrics: { key1: 'foo-metric', value1: 100 },
meta: { rangeFrom: 'now-15m', rangeTo: 'now' },
};

interaction.pageReady(pathname, eventData);

expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.endPageReady);
expect(performance.measure).toHaveBeenCalledWith(pathname, {
detail: {
eventName: 'kibana:plugin_render_time',
type: 'kibana:performance',
customMetrics: eventData.customMetrics,
meta: {
queryRangeSecs: 900,
queryOffsetSecs: 0,
},
},
end: 'end::pageReady',
start: 'start::pageChange',
});
});

it('should handle absolute date format correctly', () => {
const interaction = measureInteraction();
const pathname = '/test-path';
jest.spyOn(global.Date, 'now').mockReturnValue(1733704200000); // 2024-12-09T00:30:00Z

const eventData = {
meta: { rangeFrom: '2024-12-09T00:00:00Z', rangeTo: '2024-12-09T00:30:00Z' },
};

interaction.pageReady(pathname, eventData);

expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.endPageReady);
expect(performance.measure).toHaveBeenCalledWith(pathname, {
detail: {
eventName: 'kibana:plugin_render_time',
type: 'kibana:performance',
customMetrics: undefined,
meta: {
queryRangeSecs: 1800,
queryOffsetSecs: 0,
},
},
end: 'end::pageReady',
start: 'start::pageChange',
});
});

it('should handle negative offset when rangeTo is in the past', () => {
const interaction = measureInteraction();
const pathname = '/test-path';
jest.spyOn(global.Date, 'now').mockReturnValue(1733704200000); // 2024-12-09T00:30:00Z

const eventData = {
meta: { rangeFrom: '2024-12-08T00:00:00Z', rangeTo: '2024-12-09T00:00:00Z' },
};

interaction.pageReady(pathname, eventData);

expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.endPageReady);
expect(performance.measure).toHaveBeenCalledWith(pathname, {
detail: {
eventName: 'kibana:plugin_render_time',
type: 'kibana:performance',
customMetrics: undefined,
meta: {
queryRangeSecs: 86400,
queryOffsetSecs: -1800,
},
},
end: 'end::pageReady',
start: 'start::pageChange',
});
});

it('should handle positive offset when rangeTo is in the future', () => {
const interaction = measureInteraction();
const pathname = '/test-path';
jest.spyOn(global.Date, 'now').mockReturnValue(1733704200000); // 2024-12-09T00:30:00Z

const eventData = {
meta: { rangeFrom: '2024-12-08T01:00:00Z', rangeTo: '2024-12-09T01:00:00Z' },
};

interaction.pageReady(pathname, eventData);

expect(performance.mark).toHaveBeenCalledWith(perfomanceMarkers.endPageReady);
expect(performance.measure).toHaveBeenCalledWith(pathname, {
detail: {
eventName: 'kibana:plugin_render_time',
type: 'kibana:performance',
customMetrics: undefined,
meta: {
queryRangeSecs: 86400,
queryOffsetSecs: 1800,
},
},
end: 'end::pageReady',
start: 'start::pageChange',
});
});

it('should not measure the same route twice', () => {
const interaction = measureInteraction();
const pathname = '/test-path';

interaction.pageReady(pathname);
interaction.pageReady(pathname);

expect(performance.measure).toHaveBeenCalledTimes(1);
});
});
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,18 +8,22 @@
*/

import { useEffect, useState } from 'react';
import { CustomMetrics } from './performance_context';
import type { CustomMetrics, Meta } from './performance_context';
import { usePerformanceContext } from '../../..';

export const usePageReady = (state: { customMetrics?: CustomMetrics; isReady: boolean }) => {
export const usePageReady = (state: {
customMetrics?: CustomMetrics;
isReady: boolean;
meta?: Meta;
}) => {
const { onPageReady } = usePerformanceContext();

const [isReported, setIsReported] = useState(false);

useEffect(() => {
if (state.isReady && !isReported) {
onPageReady(state.customMetrics);
onPageReady({ customMetrics: state.customMetrics, meta: state.meta });
setIsReported(true);
}
}, [isReported, onPageReady, state.customMetrics, state.isReady]);
}, [isReported, onPageReady, state.customMetrics, state.isReady, state.meta]);
};
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
Loading

0 comments on commit bd1c00f

Please sign in to comment.