mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[RUM Dashboard] Replace FID with INP (#172467)
This commit is contained in:
parent
dc88dee511
commit
96959395b0
38 changed files with 560 additions and 140 deletions
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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' },
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 ?? ''}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "最大コンテンツの描画",
|
||||
|
|
|
@ -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": "最大内容绘制",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
226
x-pack/plugins/ux/e2e/journeys/inp.journey.ts
Normal file
226
x-pack/plugins/ux/e2e/journeys/inp.journey.ts
Normal 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,
|
||||
];
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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,
|
||||
}}
|
||||
|
|
|
@ -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}`,
|
||||
};
|
||||
};
|
||||
|
|
41
x-pack/plugins/ux/public/hooks/use_inp_query.ts
Normal file
41
x-pack/plugins/ux/public/hooks/use_inp_query.ts
Normal 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 };
|
||||
}
|
|
@ -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 };
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
81
x-pack/plugins/ux/public/services/data/inp_query.ts
Normal file
81
x-pack/plugins/ux/public/services/data/inp_query.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
|
@ -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: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue