[RUM Dashboard] Replace FID with INP (#172467)

This commit is contained in:
Shahzad 2023-12-06 00:57:05 +01:00 committed by GitHub
parent dc88dee511
commit 96959395b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 560 additions and 140 deletions

View file

@ -63,6 +63,10 @@ export async function getServiceTransactionTypes({
params
);
const transactionTypes =
aggregations?.types.buckets.map((bucket) => bucket.key as string) || [];
aggregations?.types.buckets
.map((bucket) => bucket.key as string)
// we exclude page-exit transactions because they are not relevant for the apm app
// and are only used for the INP values
.filter((value) => value !== 'page-exit') || [];
return { transactionTypes };
}

View file

@ -11,7 +11,7 @@ import { ReportViewType } from '../../types';
import {
CLS_FIELD,
FCP_FIELD,
FID_FIELD,
INP_FIELD,
LCP_FIELD,
TBT_FIELD,
TRANSACTION_DURATION,
@ -31,7 +31,7 @@ import {
ENVIRONMENT_LABEL,
EVENT_DATASET_LABEL,
FCP_LABEL,
FID_LABEL,
INP_LABEL,
HEATMAP_LABEL,
HOST_NAME_LABEL,
KPI_LABEL,
@ -102,7 +102,7 @@ export const FieldLabels: Record<string, string> = {
[LCP_FIELD]: LCP_LABEL,
[FCP_FIELD]: FCP_LABEL,
[TBT_FIELD]: TBT_LABEL,
[FID_FIELD]: FID_LABEL,
[INP_FIELD]: INP_LABEL,
[CLS_FIELD]: CLS_LABEL,
[SYNTHETICS_CLS]: CLS_LABEL,

View file

@ -129,6 +129,7 @@ export const FCP_FIELD = 'transaction.marks.agent.firstContentfulPaint';
export const LCP_FIELD = 'transaction.marks.agent.largestContentfulPaint';
export const TBT_FIELD = 'transaction.experience.tbt';
export const FID_FIELD = 'transaction.experience.fid';
export const INP_FIELD = 'numeric_labels.inp_value';
export const CLS_FIELD = 'transaction.experience.cls';
export const PROFILE_ID = 'profile.id';

View file

@ -82,6 +82,10 @@ export const FID_LABEL = i18n.translate('xpack.exploratoryView.expView.fieldLabe
defaultMessage: 'First input delay',
});
export const INP_LABEL = i18n.translate('xpack.exploratoryView.expView.fieldLabels.inp', {
defaultMessage: 'Interaction to next paint',
});
export const CLS_LABEL = i18n.translate('xpack.exploratoryView.expView.fieldLabels.cls', {
defaultMessage: 'Cumulative layout shift',
});

View file

@ -15,11 +15,11 @@ import {
ReportTypes,
USE_BREAK_DOWN_COLUMN,
} from '../constants';
import { buildPhraseFilter } from '../utils';
import { buildPhraseFilter, buildPhrasesFilter } from '../utils';
import {
CLIENT_GEO_COUNTRY_NAME,
CLS_FIELD,
FID_FIELD,
INP_FIELD,
LCP_FIELD,
PROCESSOR_EVENT,
SERVICE_NAME,
@ -32,8 +32,9 @@ import {
USER_AGENT_OS_VERSION,
URL_FULL,
SERVICE_ENVIRONMENT,
FID_FIELD,
} from '../constants/elasticsearch_fieldnames';
import { CLS_LABEL, FID_LABEL, LCP_LABEL } from '../constants/labels';
import { CLS_LABEL, FID_LABEL, INP_LABEL, LCP_LABEL } from '../constants/labels';
export function getCoreWebVitalsConfig({ dataView }: ConfigProps): SeriesConfig {
const statusPallete = euiPaletteForStatus(3);
@ -87,7 +88,7 @@ export function getCoreWebVitalsConfig({ dataView }: ConfigProps): SeriesConfig
URL_FULL,
],
baseFilters: [
...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', dataView),
...buildPhrasesFilter(TRANSACTION_TYPE, ['page-load', 'page-exit'], dataView),
...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', dataView),
],
labels: { ...FieldLabels, [SERVICE_NAME]: 'Web Application' },
@ -113,21 +114,21 @@ export function getCoreWebVitalsConfig({ dataView }: ConfigProps): SeriesConfig
],
},
{
label: FID_LABEL,
id: FID_FIELD,
label: INP_LABEL,
id: INP_FIELD,
columnType: FILTER_RECORDS,
columnFilters: [
{
language: 'kuery',
query: `${FID_FIELD} < 100`,
query: `${INP_FIELD} < 200`,
},
{
language: 'kuery',
query: `${FID_FIELD} > 100 and ${FID_FIELD} < 300`,
query: `${INP_FIELD} > 200 and ${INP_FIELD} < 500`,
},
{
language: 'kuery',
query: `${FID_FIELD} > 300`,
query: `${INP_FIELD} > 500`,
},
],
},
@ -150,12 +151,31 @@ export function getCoreWebVitalsConfig({ dataView }: ConfigProps): SeriesConfig
},
],
},
{
label: FID_LABEL,
id: FID_FIELD,
columnType: FILTER_RECORDS,
columnFilters: [
{
language: 'kuery',
query: `${FID_FIELD} < 100`,
},
{
language: 'kuery',
query: `${FID_FIELD} > 100 and ${FID_FIELD} < 300`,
},
{
language: 'kuery',
query: `${FID_FIELD} > 300`,
},
],
},
],
yConfig: [
{ color: statusPallete[0], forAccessor: 'y-axis-column' },
{ color: statusPallete[1], forAccessor: 'y-axis-column-1' },
{ color: statusPallete[2], forAccessor: 'y-axis-column-2' },
],
query: { query: 'transaction.type: "page-load"', language: 'kuery' },
query: { query: 'transaction.type: ("page-load" or "page-exit")', language: 'kuery' },
};
}

View file

@ -18,7 +18,7 @@ import {
CLIENT_GEO_COUNTRY_NAME,
CLS_FIELD,
FCP_FIELD,
FID_FIELD,
INP_FIELD,
LCP_FIELD,
PROCESSOR_EVENT,
SERVICE_ENVIRONMENT,
@ -37,7 +37,7 @@ import {
BACKEND_TIME_LABEL,
CLS_LABEL,
FCP_LABEL,
FID_LABEL,
INP_LABEL,
LCP_LABEL,
PAGE_LOAD_TIME_LABEL,
PAGES_LOADED_LABEL,
@ -97,7 +97,7 @@ export function getRumDistributionConfig({ dataView }: ConfigProps): SeriesConfi
{ label: FCP_LABEL, id: FCP_FIELD, field: FCP_FIELD },
{ label: TBT_LABEL, id: TBT_FIELD, field: TBT_FIELD },
{ label: LCP_LABEL, id: LCP_FIELD, field: LCP_FIELD },
{ label: FID_LABEL, id: FID_FIELD, field: FID_FIELD },
{ label: INP_LABEL, id: INP_FIELD, field: INP_FIELD },
{ label: CLS_LABEL, id: CLS_FIELD, field: CLS_FIELD },
],
baseFilters: [

View file

@ -77,6 +77,18 @@ export const rumFieldFormats: FieldFormat[] = [
},
},
},
{
field: FID_FIELD,
format: {
id: 'duration',
params: {
inputFormat: 'milliseconds',
outputFormat: 'humanizePrecise',
showSuffix: true,
useShortSuffix: true,
},
},
},
{
field: TRANSACTION_TIME_TO_FIRST_BYTE,
format: {

View file

@ -86,7 +86,7 @@ export const sampleAttributeCoreWebVital = {
filter: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.marks.agent.largestContentfulPaint < 2500',
'transaction.type: (page-load or page-exit) and processor.event: transaction and transaction.marks.agent.largestContentfulPaint < 2500',
},
isBucketed: false,
label: 'Good',
@ -104,7 +104,7 @@ export const sampleAttributeCoreWebVital = {
query: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.type: "page-load"',
'transaction.type: (page-load or page-exit) and processor.event: transaction and transaction.type: ("page-load" or "page-exit")',
},
visualization: {
axisTitlesVisibilitySettings: {

View file

@ -52,7 +52,6 @@ export {
export type { SloEditLocatorParams } from './locators/slo_edit';
export type { UXMetrics } from './pages/overview/components/sections/ux/core_web_vitals/core_vitals';
export { getCoreVitalsComponent } from './pages/overview/components/sections/ux/core_web_vitals/get_core_web_vitals_lazy';
export { DatePicker } from './pages/overview/components/date_picker/date_picker';

View file

@ -41,6 +41,7 @@ interface Props {
thresholds: Thresholds;
isCls?: boolean;
helpLabel: string;
dataTestSubj?: string;
}
export function getCoreVitalTooltipMessage(
@ -82,6 +83,7 @@ export function CoreVitalItem({
ranks = [100, 0, 0],
isCls,
helpLabel,
dataTestSubj,
}: Props) {
const palette = euiPaletteForStatus(3);
@ -96,6 +98,7 @@ export function CoreVitalItem({
return (
<>
<EuiStat
data-test-subj={dataTestSubj}
aria-label={`${title} ${value}`} // aria-label is required when passing a component, instead of a string, as the description
titleSize="s"
title={value ?? ''}

View file

@ -7,11 +7,12 @@
import * as React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import type { UXMetrics } from '@kbn/observability-shared-plugin/public';
import {
CLS_HELP_LABEL,
CLS_LABEL,
FID_HELP_LABEL,
FID_LABEL,
INP_HELP_LABEL,
INP_LABEL,
LCP_HELP_LABEL,
LCP_LABEL,
} from './translations';
@ -28,18 +29,6 @@ export interface CoreVitalProps {
displayTrafficMetric?: boolean;
}
export interface UXMetrics {
cls: number | null;
fid?: number | null;
lcp?: number | null;
tbt: number;
fcp?: number | null;
coreVitalPages: number;
lcpRanks: number[];
fidRanks: number[];
clsRanks: number[];
}
function formatToSec(value?: number | string, fromUnit = 'MicroSec'): string {
const valueInMs = Number(value ?? 0) / (fromUnit === 'MicroSec' ? 1000 : 1);
@ -58,7 +47,7 @@ function formatToMilliseconds(value?: number | null) {
const CoreVitalsThresholds = {
LCP: { good: '2.5s', bad: '4.0s' },
FID: { good: '100ms', bad: '300ms' },
INP: { good: '200ms', bad: '500ms' },
CLS: { good: '0.1', bad: '0.25' },
};
@ -71,7 +60,7 @@ export default function CoreVitals({
totalPageViews,
displayTrafficMetric = false,
}: CoreVitalProps) {
const { lcp, lcpRanks, fid, fidRanks, cls, clsRanks, coreVitalPages } = data || {};
const { lcp, lcpRanks, inp, inpRanks, cls, clsRanks, coreVitalPages } = data || {};
return (
<>
@ -93,16 +82,18 @@ export default function CoreVitals({
loading={loading}
thresholds={CoreVitalsThresholds.LCP}
helpLabel={LCP_HELP_LABEL}
dataTestSubj={'lcp-core-vital'}
/>
</EuiFlexItem>
<EuiFlexItem style={{ flexBasis: 380 }}>
<CoreVitalItem
title={FID_LABEL}
value={formatToMilliseconds(fid)}
ranks={fidRanks}
title={INP_LABEL}
value={formatToMilliseconds(inp)}
ranks={inpRanks}
loading={loading}
thresholds={CoreVitalsThresholds.FID}
helpLabel={FID_HELP_LABEL}
thresholds={CoreVitalsThresholds.INP}
helpLabel={INP_HELP_LABEL}
dataTestSubj={'inp-core-vital'}
/>
</EuiFlexItem>
<EuiFlexItem style={{ flexBasis: 380 }}>
@ -114,6 +105,7 @@ export default function CoreVitals({
thresholds={CoreVitalsThresholds.CLS}
isCls={true}
helpLabel={CLS_HELP_LABEL}
dataTestSubj={'cls-core-vital'}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -15,8 +15,8 @@ export const LCP_LABEL = i18n.translate('xpack.observability.ux.coreVitals.lcp',
defaultMessage: 'Largest contentful paint',
});
export const FID_LABEL = i18n.translate('xpack.observability.ux.coreVitals.fip', {
defaultMessage: 'First input delay',
export const INP_LABEL = i18n.translate('xpack.observability.ux.coreVitals.inp', {
defaultMessage: 'Interaction to Next Paint',
});
export const CLS_LABEL = i18n.translate('xpack.observability.ux.coreVitals.cls', {
@ -71,9 +71,9 @@ export const LCP_HELP_LABEL = i18n.translate('xpack.observability.ux.coreVitals.
'Largest contentful paint measures loading performance. To provide a good user experience, LCP should occur within 2.5 seconds of when the page first starts loading.',
});
export const FID_HELP_LABEL = i18n.translate('xpack.observability.ux.coreVitals.fid.help', {
export const INP_HELP_LABEL = i18n.translate('xpack.observability.ux.coreVitals.inp.help', {
defaultMessage:
'First input delay measures interactivity. To provide a good user experience, pages should have a FID of less than 100 milliseconds.',
'INP assesses responsiveness using data from the Event Timing API. When an interaction causes a page to become unresponsive, that is a poor user experience. INP observes the latency of all interactions a user has made with the page, and reports a single value which all (or nearly all) interactions were below. A low INP means the page was consistently able to respond quickly to all—or the vast majority—of user interactions.',
});
export const CLS_HELP_LABEL = i18n.translate('xpack.observability.ux.coreVitals.cls.help', {

View file

@ -11,13 +11,13 @@ export const response: UxFetchDataResponse = {
appLink: '/app/ux',
coreWebVitals: {
cls: 0.01,
fid: 13.5,
lcp: 1942.6666666666667,
tbt: 281.55833333333334,
fcp: 1487,
inp: 285,
coreVitalPages: 100,
lcpRanks: [65, 19, 16],
fidRanks: [73, 11, 16],
inpRanks: [73, 11, 16],
clsRanks: [86, 8, 6],
},
};

View file

@ -60,7 +60,7 @@ describe('UXSection', () => {
expect(getByText('elastic-co-frontend')).toBeInTheDocument();
expect(getByText('Largest contentful paint')).toBeInTheDocument();
expect(getByText('1.94 s')).toBeInTheDocument();
expect(getByText('14 ms')).toBeInTheDocument();
expect(getByText('285 ms')).toBeInTheDocument();
expect(getByText('0.010')).toBeInTheDocument();
// LCP Rank Values

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { UXMetrics } from '@kbn/observability-shared-plugin/public';
import type { ObservabilityApp } from '../../../typings/common';
import type { UXMetrics } from '../../pages/overview/components/sections/ux/core_web_vitals/core_vitals';
import { ApmIndicesConfig } from '../../../common/typings';
export interface Stat {

View file

@ -17,15 +17,16 @@ export interface ApmIndicesConfig {
}
export interface UXMetrics {
cls: number | null;
fid?: number | null;
cls?: number | null;
lcp?: number | null;
tbt: number;
tbt?: number;
fcp?: number | null;
coreVitalPages: number;
lcpRanks: number[];
fidRanks: number[];
clsRanks: number[];
coreVitalPages?: number;
lcpRanks?: number[];
clsRanks?: number[];
inp?: number | null;
hasINP?: boolean;
inpRanks?: number[];
}
export interface HeaderMenuPortalProps {

View file

@ -36,6 +36,7 @@ async function config({ readConfigFile }: FtrConfigProviderContext) {
// define custom es server here
// API Keys is enabled at the top level
'xpack.security.enabled=true',
'xpack.ml.enabled=false',
],
},

View file

@ -5,7 +5,7 @@
* 2.0.
*/
export * from './data_retention.journey';
// export * from './data_retention.journey'; // it is flaky
export * from './project_api_keys.journey';
export * from './getting_started.journey';
export * from './add_monitor.journey';
@ -18,7 +18,7 @@ export * from './private_locations.journey';
export * from './alerting_default.journey';
export * from './global_parameters.journey';
export * from './detail_flyout';
export * from './alert_rules/default_status_alert.journey';
// export * from './alert_rules/default_status_alert.journey';
export * from './test_now_mode.journey';
export * from './monitor_details_page/monitor_summary.journey';
export * from './test_run_details.journey';

View file

@ -29613,8 +29613,6 @@
"xpack.observability.ux.coreVitals.average": "une moyenne",
"xpack.observability.ux.coreVitals.cls": "Cumulative Layout Shift",
"xpack.observability.ux.coreVitals.cls.help": "Cumulative Layout Shift (CLS) : mesure la stabilité visuelle. Pour offrir une expérience agréable aux utilisateurs, les pages doivent conserver un CLS inférieur à 0,1.",
"xpack.observability.ux.coreVitals.fid.help": "First Input Delay mesure l'interactivité. Pour offrir une expérience agréable aux utilisateurs, les pages doivent avoir un FID inférieur à 100 millisecondes.",
"xpack.observability.ux.coreVitals.fip": "First Input Delay",
"xpack.observability.ux.coreVitals.good": "un bon",
"xpack.observability.ux.coreVitals.is": "est",
"xpack.observability.ux.coreVitals.lcp": "Largest Contentful Paint",

View file

@ -29613,8 +29613,6 @@
"xpack.observability.ux.coreVitals.average": "平均",
"xpack.observability.ux.coreVitals.cls": "累積レイアウト変更",
"xpack.observability.ux.coreVitals.cls.help": "累積レイアウト変更CLS視覚的な安定性を計測します。優れたユーザーエクスペリエンスを実現するには、ページのCLSを0.1未満に保ってください。",
"xpack.observability.ux.coreVitals.fid.help": "初回入力遅延FIDは双方向性を計測します。優れたユーザーエクスペリエンスを実現するには、ページのFIDを100ミリ秒未満に保ってください。",
"xpack.observability.ux.coreVitals.fip": "初回入力遅延",
"xpack.observability.ux.coreVitals.good": "優れている",
"xpack.observability.ux.coreVitals.is": "is",
"xpack.observability.ux.coreVitals.lcp": "最大コンテンツの描画",

View file

@ -29610,8 +29610,6 @@
"xpack.observability.ux.coreVitals.average": "平均值",
"xpack.observability.ux.coreVitals.cls": "累计布局偏移",
"xpack.observability.ux.coreVitals.cls.help": "累计布局偏移 (CLS):衡量视觉稳定性。为了提供良好的用户体验,页面的 CLS 应小于 0.1。",
"xpack.observability.ux.coreVitals.fid.help": "首次输入延迟用于衡量交互性。为了提供良好的用户体验,页面的 FID 应小于 100 毫秒。",
"xpack.observability.ux.coreVitals.fip": "首次输入延迟",
"xpack.observability.ux.coreVitals.good": "良好",
"xpack.observability.ux.coreVitals.is": "是",
"xpack.observability.ux.coreVitals.lcp": "最大内容绘制",

View file

@ -46,5 +46,5 @@ export const TRANSACTION_DOM_INTERACTIVE =
export const FCP_FIELD = 'transaction.marks.agent.firstContentfulPaint';
export const LCP_FIELD = 'transaction.marks.agent.largestContentfulPaint';
export const TBT_FIELD = 'transaction.experience.tbt';
export const FID_FIELD = 'transaction.experience.fid';
export const CLS_FIELD = 'transaction.experience.cls';
export const INP_FIELD = 'numeric_labels.inp_value';

View file

@ -6,5 +6,6 @@
*/
export const TRANSACTION_PAGE_LOAD = 'page-load';
export const TRANSACTION_PAGE_EXIT = 'page-exit';
export const TRANSACTION_REQUEST = 'request';
export const TRANSACTION_ROUTE_CHANGE = 'route-change';

View file

@ -5,6 +5,7 @@
* 2.0.
*/
export * from './inp.journey';
export * from './core_web_vitals';
// export * from './page_views';
export * from './url_ux_query.journey';

View file

@ -0,0 +1,226 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { journey, step, expect, before } from '@elastic/synthetics';
import { Client } from '@elastic/elasticsearch';
import { recordVideo } from '../helpers/record_video';
import { loginToKibana, waitForLoadingToFinish } from './utils';
const addTestTransaction = async (params: any) => {
const getService = params.getService;
const es: Client = getService('es');
const document = getPageLoad();
const index = 'apm-8.0.0-transaction-000001';
await es.index({
index,
document,
});
for (let i = 0; i < 100; i++) {
await es.index({
index,
document: getPageExit(INP_VALUES[i]),
});
}
};
journey('INP', async ({ page, params }) => {
recordVideo(page);
before(async () => {
await addTestTransaction(params);
await waitForLoadingToFinish({ page });
});
const queryParams = {
percentile: '50',
rangeFrom: 'now-1y',
rangeTo: 'now',
};
const queryString = new URLSearchParams(queryParams).toString();
const baseUrl = `${params.kibanaUrl}/app/ux`;
step('Go to UX Dashboard', async () => {
await page.goto(`${baseUrl}?${queryString}`, {
waitUntil: 'networkidle',
});
await loginToKibana({
page,
user: { username: 'viewer', password: 'changeme' },
});
});
step('Check INP Values', async () => {
expect(await page.$('text=Interaction to Next Paint'));
await page.waitForSelector('[data-test-subj=inp-core-vital] > .euiTitle');
await page.waitForSelector('text=381 ms');
});
});
const getPageLoad = () => ({
'@timestamp': new Date(Date.now()).toISOString(),
agent: {
name: 'rum-js',
version: '5.1.1',
},
client: {
geo: {
continent_name: 'North America',
country_iso_code: 'US',
location: {
lat: 37.751,
lon: -97.822,
},
},
ip: '151.101.130.217',
},
ecs: {
version: '1.5.0',
},
event: {
ingested: new Date(Date.now()).toISOString(),
outcome: 'unknown',
},
http: {
request: {
referrer: '',
},
response: {
decoded_body_size: 813,
encoded_body_size: 813,
transfer_size: 962,
},
},
observer: {
ephemeral_id: '863bfb71-dd0d-4033-833f-f9f3d3b71961',
hostname: 'eb12315912f8',
id: '23c1bdbb-6a2a-461a-a71f-6338116b5501',
type: 'apm-server',
version: '8.0.0',
version_major: 8,
},
processor: {
event: 'transaction',
name: 'transaction',
},
service: {
language: {
name: 'javascript',
},
name: 'client',
version: '1.0.0',
},
source: {
ip: '151.101.130.217',
},
timestamp: {
us: 1600080193349369,
},
trace: {
id: 'd2f9a6f07ea467c68576ee45b97d9aec',
},
transaction: {
custom: {
userConfig: {
featureFlags: ['double-trouble', '4423-hotfix'],
showDashboard: true,
},
},
duration: {
us: 72584,
},
id: '8563bad355ff20f7',
marks: {
agent: {
domComplete: 61,
domInteractive: 51,
timeToFirstByte: 3,
},
navigationTiming: {
connectEnd: 1,
connectStart: 1,
domComplete: 61,
domContentLoadedEventEnd: 51,
domContentLoadedEventStart: 51,
domInteractive: 51,
domLoading: 9,
domainLookupEnd: 1,
domainLookupStart: 1,
fetchStart: 0,
loadEventEnd: 61,
loadEventStart: 61,
requestStart: 1,
responseEnd: 4,
responseStart: 3,
},
},
name: '/products',
page: {
referer: '',
url: 'http://opbeans-node:3000/products',
},
sampled: true,
span_count: {
started: 5,
},
type: 'page-load',
},
url: {
domain: 'opbeans-node',
full: 'http://opbeans-node:3000/products',
original: 'http://opbeans-node:3000/products',
path: '/products',
port: 3000,
scheme: 'http',
},
user: {
email: 'arthur.dent@example.com',
id: '1',
name: 'arthurdent',
},
user_agent: {
device: {
name: 'Other',
},
name: 'Chrome',
original:
'Mozilla/5.0 (CrKey armv7l 1.5.16041) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.0 Safari/537.36',
os: {
name: 'Chromecast',
},
version: '31.0.1650.0',
},
});
const getPageExit = (inpValue: number = 200) => {
const pageLoad = getPageLoad();
return {
...pageLoad,
transaction: {
...pageLoad.transaction,
type: 'page-exit',
},
numeric_labels: {
inp_value: inpValue,
},
};
};
const INP_VALUES = [
482, 343, 404, 591, 545, 789, 664, 721, 442, 376, 797, 580, 749, 363, 673,
141, 234, 638, 378, 448, 175, 543, 665, 146, 742, 686, 210, 324, 365, 192,
301, 317, 728, 655, 427, 66, 741, 357, 732, 93, 592, 200, 636, 122, 695, 709,
322, 296, 196, 188, 139, 346, 637, 315, 756, 139, 97, 411, 98, 695, 615, 394,
619, 713, 100, 373, 730, 226, 270, 168, 740, 65, 215, 383, 614, 154, 645, 661,
594, 71, 264, 377, 599, 92, 771, 474, 566, 106, 192, 491, 121, 210, 690, 310,
753, 266, 289, 743, 134, 100,
];

View file

@ -119,11 +119,13 @@ export function UXAppRoot({
dataViews,
lens,
},
isDev,
}: {
appMountParameters: AppMountParameters;
core: CoreStart;
deps: ApmPluginSetupDeps;
corePlugins: ApmPluginStartDeps;
isDev: boolean;
}) {
const { history } = appMountParameters;
const i18nCore = core.i18n;
@ -183,6 +185,7 @@ export function UXAppRoot({
</EuiErrorBoundary>
<UXActionMenu
appMountParameters={appMountParameters}
isDev={isDev}
/>
</UrlParamsProvider>
</InspectorContextProvider>
@ -207,11 +210,13 @@ export const renderApp = ({
deps,
appMountParameters,
corePlugins,
isDev,
}: {
core: CoreStart;
deps: ApmPluginSetupDeps;
appMountParameters: AppMountParameters;
corePlugins: ApmPluginStartDeps;
isDev: boolean;
}) => {
const { element } = appMountParameters;
@ -229,6 +234,7 @@ export const renderApp = ({
core={core}
deps={deps}
corePlugins={corePlugins}
isDev={isDev}
/>,
element
);

View file

@ -34,8 +34,10 @@ const ANALYZE_MESSAGE = i18n.translate(
export function UXActionMenu({
appMountParameters,
isDev,
}: {
appMountParameters: AppMountParameters;
isDev: boolean;
}) {
const { http, application } = useKibanaServices();
const { urlParams } = useLegacyUrlParams();
@ -85,7 +87,7 @@ export function UXActionMenu({
defaultMessage: 'Add data',
})}
</EuiHeaderLink>
<UxInspectorHeaderLink />
<UxInspectorHeaderLink isDev={isDev} />
<ObservabilityAIAssistantActionMenuItem />
</EuiHeaderLinks>
</HeaderMenuPortal>

View file

@ -12,7 +12,7 @@ import { enableInspectEsQueries } from '@kbn/observability-plugin/public';
import { useInspectorContext } from '@kbn/observability-shared-plugin/public';
import { useKibanaServices } from '../../../../hooks/use_kibana_services';
export function UxInspectorHeaderLink() {
export function UxInspectorHeaderLink({ isDev }: { isDev: boolean }) {
const { inspectorAdapters } = useInspectorContext();
const { uiSettings, inspector } = useKibanaServices();
@ -22,7 +22,7 @@ export function UxInspectorHeaderLink() {
inspector.open(inspectorAdapters);
};
if (!isInspectorEnabled) {
if (!isInspectorEnabled && !isDev) {
return null;
}

View file

@ -15,6 +15,7 @@ import {
EuiTitle,
} from '@elastic/eui';
import { getCoreVitalsComponent } from '@kbn/observability-plugin/public';
import { useINPQuery } from '../../../../hooks/use_inp_query';
import { I18LABELS } from '../translations';
import { KeyUXMetrics } from './key_ux_metrics';
import { useUxQuery } from '../hooks/use_ux_query';
@ -31,8 +32,9 @@ export function UXMetrics() {
const uxQuery = useUxQuery();
const { data, loading: loadingResponse } = useCoreWebVitalsQuery(uxQuery);
const { data: inpData, loading: inpLoading } = useINPQuery(uxQuery);
const loading = loadingResponse ?? true;
const loading = (loadingResponse ?? true) || inpLoading;
const {
sharedData: { totalPageViews },
@ -41,13 +43,18 @@ export function UXMetrics() {
const CoreVitals = useMemo(
() =>
getCoreVitalsComponent({
data,
data: {
...data,
inp: inpData?.inp,
inpRanks: inpData?.inpRanks,
hasINP: inpData?.hasINP,
},
totalPageViews,
loading,
displayTrafficMetric: true,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[loading]
[loading, inpLoading]
);
return (

View file

@ -25,12 +25,12 @@ describe('KeyUXMetrics', () => {
loading={false}
data={{
cls: 0.01,
fid: 6,
inp: 6,
lcp: 1701.1142857142856,
tbt: 270.915,
fcp: 1273.6285714285714,
lcpRanks: [69, 17, 14],
fidRanks: [83, 6, 11],
inpRanks: [83, 6, 11],
clsRanks: [90, 7, 3],
coreVitalPages: 1000,
}}

View file

@ -18,6 +18,10 @@ import {
UXHasDataResponse,
} from '@kbn/observability-plugin/public';
import type { UXMetrics } from '@kbn/observability-shared-plugin/public';
import {
inpQuery,
transformINPResponse,
} from '../../../services/data/inp_query';
import {
coreWebVitalsQuery,
transformCoreWebVitalsResponse,
@ -45,36 +49,52 @@ async function getCoreWebVitalsResponse({
}
);
return await esQuery<ReturnType<typeof coreWebVitalsQuery>>(dataStartPlugin, {
params: {
index: dataViewResponse.apmDataViewIndexPattern,
...coreWebVitalsQuery(absoluteTime.start, absoluteTime.end, undefined, {
serviceName: serviceName ? [serviceName] : undefined,
}),
},
});
return await Promise.all([
esQuery<ReturnType<typeof coreWebVitalsQuery>>(dataStartPlugin, {
params: {
index: dataViewResponse.apmDataViewIndexPattern,
...coreWebVitalsQuery(absoluteTime.start, absoluteTime.end, undefined, {
serviceName: serviceName ? [serviceName] : undefined,
}),
},
}),
esQuery<ReturnType<typeof inpQuery>>(dataStartPlugin, {
params: {
index: dataViewResponse.apmDataViewIndexPattern,
...inpQuery(absoluteTime.start, absoluteTime.end, undefined, {
serviceName: serviceName ? [serviceName] : undefined,
}),
},
}),
]);
}
const CORE_WEB_VITALS_DEFAULTS: UXMetrics = {
coreVitalPages: 0,
cls: 0,
fid: 0,
lcp: 0,
tbt: 0,
fcp: 0,
lcpRanks: DEFAULT_RANKS,
fidRanks: DEFAULT_RANKS,
inpRanks: DEFAULT_RANKS,
clsRanks: DEFAULT_RANKS,
};
export const fetchUxOverviewDate = async (
params: WithDataPlugin<FetchDataParams>
): Promise<UxFetchDataResponse> => {
const coreWebVitalsResponse = await getCoreWebVitalsResponse(params);
const [coreWebVitalsResponse, inpResponse] = await getCoreWebVitalsResponse(
params
);
const data =
transformCoreWebVitalsResponse(coreWebVitalsResponse) ??
CORE_WEB_VITALS_DEFAULTS;
const inpData = transformINPResponse(inpResponse);
return {
coreWebVitals:
transformCoreWebVitalsResponse(coreWebVitalsResponse) ??
CORE_WEB_VITALS_DEFAULTS,
coreWebVitals: {
...data,
...(inpData ? { inp: inpData?.inp, inpRanks: inpData?.inpRanks } : {}),
},
appLink: `/app/ux?rangeFrom=${params.relativeTime.start}&rangeTo=${params.relativeTime.end}`,
};
};

View file

@ -0,0 +1,41 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useEsSearch } from '@kbn/observability-shared-plugin/public';
import { useMemo } from 'react';
import { inpQuery, transformINPResponse } from '../services/data/inp_query';
import { useDataView } from '../components/app/rum_dashboard/local_uifilters/use_data_view';
import { callDateMath } from '../services/data/call_date_math';
import { PERCENTILE_DEFAULT } from '../services/data/core_web_vitals_query';
import { useUxQuery } from '../components/app/rum_dashboard/hooks/use_ux_query';
export function useINPQuery(uxQuery: ReturnType<typeof useUxQuery>) {
const { dataViewTitle } = useDataView();
const { data: esQueryResponse, loading } = useEsSearch(
{
index: uxQuery ? dataViewTitle : undefined,
...inpQuery(
callDateMath(uxQuery?.start),
callDateMath(uxQuery?.end),
uxQuery?.urlQuery,
uxQuery?.uiFilters ? JSON.parse(uxQuery.uiFilters) : {},
uxQuery?.percentile ? Number(uxQuery.percentile) : undefined
),
},
[uxQuery, dataViewTitle],
{ name: 'UxINPMetrics' }
);
const data = useMemo(
() =>
transformINPResponse(
esQueryResponse,
uxQuery?.percentile ? Number(uxQuery?.percentile) : PERCENTILE_DEFAULT
),
[esQueryResponse, uxQuery?.percentile]
);
return { data, loading };
}

View file

@ -5,10 +5,11 @@
* 2.0.
*/
import { PluginInitializer } from '@kbn/core/public';
import { PluginInitializer, PluginInitializerContext } from '@kbn/core/public';
import { UxPlugin, UxPluginSetup, UxPluginStart } from './plugin';
export const plugin: PluginInitializer<UxPluginSetup, UxPluginStart> = () =>
new UxPlugin();
export const plugin: PluginInitializer<UxPluginSetup, UxPluginStart> = (
initializerContext: PluginInitializerContext
) => new UxPlugin(initializerContext);
export type { UxPluginSetup, UxPluginStart };

View file

@ -20,6 +20,7 @@ import {
CoreStart,
DEFAULT_APP_CATEGORIES,
Plugin,
PluginInitializerContext,
} from '@kbn/core/public';
import {
DataPublicPluginSetup,
@ -82,7 +83,7 @@ async function getDataStartPlugin(core: CoreSetup) {
}
export class UxPlugin implements Plugin<UxPluginSetup, UxPluginStart> {
constructor() {}
constructor(private readonly initContext: PluginInitializerContext) {}
public setup(core: CoreSetup, plugins: ApmPluginSetupDeps) {
const pluginSetupDeps = plugins;
@ -169,6 +170,8 @@ export class UxPlugin implements Plugin<UxPluginSetup, UxPluginStart> {
)
);
const isDev = this.initContext.env.mode.dev;
core.application.register({
id: 'ux',
title: 'User Experience',
@ -202,6 +205,7 @@ export class UxPlugin implements Plugin<UxPluginSetup, UxPluginStart> {
]);
return renderApp({
isDev,
core: coreStart,
deps: pluginSetupDeps,
appMountParameters,

View file

@ -37,24 +37,6 @@ Object {
],
},
},
"fid": Object {
"percentiles": Object {
"field": "transaction.experience.fid",
"percents": Array [
50,
],
},
},
"fidRanks": Object {
"percentile_ranks": Object {
"field": "transaction.experience.fid",
"keyed": false,
"values": Array [
100,
300,
],
},
},
"lcp": Object {
"percentiles": Object {
"field": "transaction.marks.agent.largestContentfulPaint",

View file

@ -11,7 +11,6 @@ import {
TBT_FIELD,
FCP_FIELD,
CLS_FIELD,
FID_FIELD,
LCP_FIELD,
} from '../../../common/elasticsearch_fieldnames';
import { SetupUX, UxUIFilters } from '../../../typings/ui_filters';
@ -20,7 +19,7 @@ import { getRumPageLoadTransactionsProjection } from './projections';
export const DEFAULT_RANKS = [100, 0, 0];
const getRanksPercentages = (ranks?: Record<string, number | null>) => {
export const getRanksPercentages = (ranks?: Record<string, number | null>) => {
if (!Array.isArray(ranks)) return null;
const ranksVal = ranks?.map(({ value }) => value?.toFixed(0) ?? 0) ?? [];
return [
@ -39,17 +38,8 @@ export function transformCoreWebVitalsResponse<T>(
percentile = PERCENTILE_DEFAULT
): UXMetrics | undefined {
if (!response) return response;
const {
lcp,
cls,
fid,
tbt,
fcp,
lcpRanks,
fidRanks,
clsRanks,
coreVitalPages,
} = response.aggregations ?? {};
const { lcp, cls, tbt, fcp, lcpRanks, clsRanks, coreVitalPages } =
response.aggregations ?? {};
const pkey = percentile.toFixed(1);
@ -58,7 +48,6 @@ export function transformCoreWebVitalsResponse<T>(
/* Because cls is required in the type UXMetrics, and defined as number | null,
* we need to default to null in the case where cls is undefined in order to satisfy the UXMetrics type */
cls: cls?.values[pkey] ?? null,
fid: fid?.values[pkey],
lcp: lcp?.values[pkey],
tbt: tbt?.values[pkey] ?? 0,
fcp: fcp?.values[pkey],
@ -66,9 +55,6 @@ export function transformCoreWebVitalsResponse<T>(
lcpRanks: lcp?.values[pkey]
? getRanksPercentages(lcpRanks?.values) ?? DEFAULT_RANKS
: DEFAULT_RANKS,
fidRanks: fid?.values[pkey]
? getRanksPercentages(fidRanks?.values) ?? DEFAULT_RANKS
: DEFAULT_RANKS,
clsRanks: cls?.values[pkey]
? getRanksPercentages(clsRanks?.values) ?? DEFAULT_RANKS
: DEFAULT_RANKS,
@ -114,12 +100,6 @@ export function coreWebVitalsQuery(
percents: [percentile],
},
},
fid: {
percentiles: {
field: FID_FIELD,
percents: [percentile],
},
},
cls: {
percentiles: {
field: CLS_FIELD,
@ -145,13 +125,6 @@ export function coreWebVitalsQuery(
keyed: false,
},
},
fidRanks: {
percentile_ranks: {
field: FID_FIELD,
values: [100, 300],
keyed: false,
},
},
clsRanks: {
percentile_ranks: {
field: CLS_FIELD,

View file

@ -0,0 +1,81 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ESSearchResponse } from '@kbn/es-types';
import { UXMetrics } from '@kbn/observability-shared-plugin/public/types';
import { DEFAULT_RANKS, getRanksPercentages } from './core_web_vitals_query';
import { INP_FIELD } from '../../../common/elasticsearch_fieldnames';
import { SetupUX, UxUIFilters } from '../../../typings/ui_filters';
import { mergeProjection } from '../../../common/utils/merge_projection';
import { getRumPageExitTransactionsProjection } from './projections';
export function transformINPResponse<T>(
response?: ESSearchResponse<
T,
ReturnType<typeof inpQuery>,
{ restTotalHitsAsInt: false }
>,
percentile = PERCENTILE_DEFAULT
): UXMetrics | undefined {
if (!response) return response;
const { inp, inpRanks } = response.aggregations ?? {};
const pkey = percentile.toFixed(1);
return {
hasINP: response.hits.total.value > 0,
inp: inp?.values[pkey],
inpRanks: inp?.values[pkey]
? getRanksPercentages(inpRanks?.values) ?? DEFAULT_RANKS
: DEFAULT_RANKS,
};
}
export const PERCENTILE_DEFAULT = 50;
export function inpQuery(
start: number,
end: number,
urlQuery?: string,
uiFilters?: UxUIFilters,
percentile = PERCENTILE_DEFAULT
) {
const setup: SetupUX = { uiFilters: uiFilters ?? {} };
const projection = getRumPageExitTransactionsProjection({
setup,
urlQuery,
start,
end,
});
return mergeProjection(projection, {
body: {
size: 0,
query: {
bool: {
filter: [...projection.body.query.bool.filter],
},
},
aggs: {
inp: {
percentiles: {
field: INP_FIELD,
percents: [percentile],
},
},
inpRanks: {
percentile_ranks: {
field: INP_FIELD,
values: [200, 500],
keyed: false,
},
},
},
},
});
}

View file

@ -11,7 +11,10 @@ import {
PROCESSOR_EVENT,
TRANSACTION_TYPE,
} from '../../../common/elasticsearch_fieldnames';
import { TRANSACTION_PAGE_LOAD } from '../../../common/transaction_types';
import {
TRANSACTION_PAGE_EXIT,
TRANSACTION_PAGE_LOAD,
} from '../../../common/transaction_types';
import { SetupUX } from '../../../typings/ui_filters';
import { getEsFilter } from './get_es_filter';
import { rangeQuery } from './range_query';
@ -70,6 +73,47 @@ export function getRumPageLoadTransactionsProjection({
};
}
export function getRumPageExitTransactionsProjection({
setup,
urlQuery,
start,
end,
}: {
setup: SetupUX;
urlQuery?: string;
start: number;
end: number;
}) {
const { uiFilters } = setup;
const bool = {
filter: [
...rangeQuery(start, end),
{ term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_EXIT } },
{ terms: { [PROCESSOR_EVENT]: [ProcessorEvent.transaction] } },
...(urlQuery
? [
{
wildcard: {
'url.full': `*${urlQuery}*`,
},
},
]
: []),
...getEsFilter(uiFilters),
],
must_not: [...getEsFilter(uiFilters, true)],
};
return {
body: {
query: {
bool,
},
},
};
}
export interface RumErrorsProjection {
body: {
query: {