From 69ecbed3e6ca321d171c13b6a0905e057bf672d4 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 23 Aug 2021 09:50:54 -0400 Subject: [PATCH] [ML] Fix UI inconsistencies in APM Failed transaction correlations (#109187) (#109625) - Show the same empty state in the correlations table - Add "Correlations" title above the table - Add EuiSpacer between the sections before and after progress - Update the copy within the beta badge title=Failed transaction correlations description=Failed transaction correlations is not GA... - Remove s size from the beta badge - Move the help popover to the top of the panel (similar to the Latency correlations tab) - Move the Cancel/Refresh option to the right of the progress bar (similar to the Latency correlations tab) - When the correlation job is running the correlations tab should show a loading state similar to the latency correlations table - Indicate in the table headers Score is sorted by default - Add sortability to both Latency and failed transactions correlations table - Refactor to prevent duplicate code/components like Log, progress bar - Fix alignments of the tab content header (previously navigating from one Trace samples tab to Latency correlation tabs will cause a minor jump in the header, or the titles within the same tab were not the same size ) - Remove the event.outcome as a field candidate (because event.outcome: failure would always be significant in this case) - Shrink the column width for impact (previously it was at 116px) - Added badge for High, medium, low [APM] Correlations: Show impact levels (high, medium, low) as colored badges indicating their severity - Fix license prompt text - Functional tests for the new tab - Make the p value & error rate columns visible only when esInspect mode is enabled Co-authored-by: Quynh Nguyen <43350163+qn895@users.noreply.github.com> --- .../failure_correlations/types.ts | 3 + .../common/utils/formatters/datetime.test.ts | 28 ++ .../app/correlations/correlations_log.tsx | 38 ++ .../app/correlations/correlations_table.tsx | 26 +- .../cross_cluster_search_warning.tsx | 33 ++ .../app/correlations/empty_state_prompt.tsx | 49 ++ .../failed_transactions_correlations.tsx | 426 ++++++++++-------- ...transactions_correlations_help_popover.tsx | 18 +- .../app/correlations/latency_correlations.tsx | 239 ++++------ .../app/correlations/progress_controls.tsx | 77 ++++ ...nsactions_correlation_impact_label.test.ts | 42 +- ...d_transactions_correlation_impact_label.ts | 17 +- .../distribution/index.tsx | 5 +- .../failed_transactions_correlations_tab.tsx | 2 +- ...ailed_transactions_correlations_fetcher.ts | 6 +- .../async_search_service.ts | 10 +- .../queries/query_failure_correlation.ts | 19 +- .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - .../common/utils/parse_b_fetch.ts | 15 + .../tests/correlations/failed_transactions.ts | 238 ++++++++++ .../{latency_ml.ts => latency.ts} | 10 +- .../test/apm_api_integration/tests/index.ts | 9 +- .../failed_transaction_correlations.ts | 155 +++++++ .../functional/apps/apm/correlations/index.ts | 3 +- .../apm/correlations/latency_correlations.ts | 6 +- 26 files changed, 1074 insertions(+), 408 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/correlations/correlations_log.tsx create mode 100644 x-pack/plugins/apm/public/components/app/correlations/cross_cluster_search_warning.tsx create mode 100644 x-pack/plugins/apm/public/components/app/correlations/empty_state_prompt.tsx create mode 100644 x-pack/plugins/apm/public/components/app/correlations/progress_controls.tsx create mode 100644 x-pack/test/apm_api_integration/common/utils/parse_b_fetch.ts create mode 100644 x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts rename x-pack/test/apm_api_integration/tests/correlations/{latency_ml.ts => latency.ts} (97%) create mode 100644 x-pack/test/functional/apps/apm/correlations/failed_transaction_correlations.ts diff --git a/x-pack/plugins/apm/common/search_strategies/failure_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/failure_correlations/types.ts index 08e05d46ba013..2b0d2b5642e0c 100644 --- a/x-pack/plugins/apm/common/search_strategies/failure_correlations/types.ts +++ b/x-pack/plugins/apm/common/search_strategies/failure_correlations/types.ts @@ -15,6 +15,9 @@ export interface FailedTransactionsCorrelationValue { pValue: number | null; fieldName: string; fieldValue: string; + normalizedScore: number; + failurePercentage: number; + successPercentage: number; } export type FailureCorrelationImpactThreshold = typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD[keyof typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD]; diff --git a/x-pack/plugins/apm/common/utils/formatters/datetime.test.ts b/x-pack/plugins/apm/common/utils/formatters/datetime.test.ts index 9efb7184f3927..54e74330c0604 100644 --- a/x-pack/plugins/apm/common/utils/formatters/datetime.test.ts +++ b/x-pack/plugins/apm/common/utils/formatters/datetime.test.ts @@ -165,6 +165,34 @@ describe('date time formatters', () => { 'Dec 1, 2019, 13:00:00.000 (UTC+1)' ); }); + + it('milliseconds', () => { + moment.tz.setDefault('Europe/Copenhagen'); + expect(asAbsoluteDateTime(1559390400000, 'milliseconds')).toBe( + 'Jun 1, 2019, 14:00:00.000 (UTC+2)' + ); + }); + + it('seconds', () => { + moment.tz.setDefault('Europe/Copenhagen'); + expect(asAbsoluteDateTime(1559390400000, 'seconds')).toBe( + 'Jun 1, 2019, 14:00:00 (UTC+2)' + ); + }); + + it('minutes', () => { + moment.tz.setDefault('Europe/Copenhagen'); + expect(asAbsoluteDateTime(1559390400000, 'minutes')).toBe( + 'Jun 1, 2019, 14:00 (UTC+2)' + ); + }); + + it('hours', () => { + moment.tz.setDefault('Europe/Copenhagen'); + expect(asAbsoluteDateTime(1559390400000, 'hours')).toBe( + 'Jun 1, 2019, 14 (UTC+2)' + ); + }); }); describe('getDateDifference', () => { it('milliseconds', () => { diff --git a/x-pack/plugins/apm/public/components/app/correlations/correlations_log.tsx b/x-pack/plugins/apm/public/components/app/correlations/correlations_log.tsx new file mode 100644 index 0000000000000..2115918a71415 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/correlations_log.tsx @@ -0,0 +1,38 @@ +/* + * 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 { EuiAccordion, EuiCode, EuiPanel } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { asAbsoluteDateTime } from '../../../../common/utils/formatters'; + +interface Props { + logMessages: string[]; +} +export function CorrelationsLog({ logMessages }: Props) { + return ( + + + {logMessages.map((logMessage, i) => { + const [timestamp, message] = logMessage.split(': '); + return ( +

+ + {asAbsoluteDateTime(timestamp)} {message} + +

+ ); + })} +
+
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx index 28f671183ed87..f7e62b76a61c0 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx @@ -9,10 +9,12 @@ import React, { useCallback, useMemo, useState } from 'react'; import { debounce } from 'lodash'; import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import type { EuiTableSortingType } from '@elastic/eui/src/components/basic_table/table_types'; +import type { Criteria } from '@elastic/eui/src/components/basic_table/basic_table'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { useUiTracker } from '../../../../../observability/public'; import { useTheme } from '../../../hooks/use_theme'; -import { CorrelationsTerm } from '../../../../common/search_strategies/failure_correlations/types'; +import type { CorrelationsTerm } from '../../../../common/search_strategies/failure_correlations/types'; const PAGINATION_SIZE_OPTIONS = [5, 10, 20, 50]; @@ -29,6 +31,8 @@ interface Props { selectedTerm?: { fieldName: string; fieldValue: string }; onFilter?: () => void; columns: Array>; + onTableChange: (c: Criteria) => void; + sorting?: EuiTableSortingType; } export function CorrelationsTable({ @@ -37,6 +41,8 @@ export function CorrelationsTable({ setSelectedSignificantTerm, columns, selectedTerm, + onTableChange, + sorting, }: Props) { const euiTheme = useTheme(); const trackApmEvent = useUiTracker({ app: 'apm' }); @@ -67,12 +73,17 @@ export function CorrelationsTable({ }; }, [pageIndex, pageSize, significantTerms]); - const onTableChange = useCallback(({ page }) => { - const { index, size } = page; + const onChange = useCallback( + (tableSettings) => { + const { index, size } = tableSettings.page; - setPageIndex(index); - setPageSize(size); - }, []); + setPageIndex(index); + setPageSize(size); + + onTableChange(tableSettings); + }, + [onTableChange] + ); return ( ({ }; }} pagination={pagination} - onChange={onTableChange} + onChange={onChange} + sorting={sorting} /> ); } diff --git a/x-pack/plugins/apm/public/components/app/correlations/cross_cluster_search_warning.tsx b/x-pack/plugins/apm/public/components/app/correlations/cross_cluster_search_warning.tsx new file mode 100644 index 0000000000000..9d5ca09ad3add --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/cross_cluster_search_warning.tsx @@ -0,0 +1,33 @@ +/* + * 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 { EuiCallOut } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +export function CrossClusterSearchCompatibilityWarning({ + version, +}: { + version: string; +}) { + return ( + +

+ {i18n.translate('xpack.apm.correlations.ccsWarningCalloutBody', { + defaultMessage: + 'Data for the correlation analysis could not be fully retrieved. This feature is supported only for {version} and later versions.', + values: { version }, + })} +

+
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/empty_state_prompt.tsx b/x-pack/plugins/apm/public/components/app/correlations/empty_state_prompt.tsx new file mode 100644 index 0000000000000..57e57a526baff --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/empty_state_prompt.tsx @@ -0,0 +1,49 @@ +/* + * 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 { EuiEmptyPrompt, EuiSpacer, EuiText } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export function CorrelationsEmptyStatePrompt() { + return ( + <> + + +

+ {i18n.translate('xpack.apm.correlations.noCorrelationsTitle', { + defaultMessage: 'No significant correlations', + })} +

+ + } + body={ + <> + +

+ +

+

+ +

+
+ + } + /> + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx index 4fdd908b6faf6..1cc115861a594 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx @@ -5,45 +5,46 @@ * 2.0. */ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { - EuiCallOut, - EuiCode, - EuiAccordion, - EuiPanel, EuiBasicTableColumn, - EuiButton, EuiFlexGroup, EuiFlexItem, - EuiProgress, EuiSpacer, - EuiText, - EuiBadge, EuiIcon, EuiLink, EuiTitle, EuiBetaBadge, + EuiBadge, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { useHistory } from 'react-router-dom'; +import { orderBy } from 'lodash'; +import type { EuiTableSortingType } from '@elastic/eui/src/components/basic_table/table_types'; +import type { Direction } from '@elastic/eui/src/services/sort/sort_direction'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { CorrelationsTable } from './correlations_table'; import { enableInspectEsQueries } from '../../../../../observability/public'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { FailedTransactionsCorrelationsHelpPopover } from './failed_transactions_correlations_help_popover'; -import { FailedTransactionsCorrelationValue } from '../../../../common/search_strategies/failure_correlations/types'; import { ImpactBar } from '../../shared/ImpactBar'; import { isErrorMessage } from './utils/is_error_message'; -import { Summary } from '../../shared/Summary'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { getFailedTransactionsCorrelationImpactLabel } from './utils/get_failed_transactions_correlation_impact_label'; import { createHref, push } from '../../shared/Links/url_helpers'; import { useUiTracker } from '../../../../../observability/public'; import { useFailedTransactionsCorrelationsFetcher } from '../../../hooks/use_failed_transactions_correlations_fetcher'; -import { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; import { useApmParams } from '../../../hooks/use_apm_params'; +import { CorrelationsLog } from './correlations_log'; +import { CorrelationsEmptyStatePrompt } from './empty_state_prompt'; +import { CrossClusterSearchCompatibilityWarning } from './cross_cluster_search_warning'; +import { CorrelationsProgressControls } from './progress_controls'; +import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; +import type { FailedTransactionsCorrelationValue } from '../../../../common/search_strategies/failure_correlations/types'; +import { Summary } from '../../shared/Summary'; +import { asPercent } from '../../../../common/utils/formatters'; export function FailedTransactionsCorrelations({ onFilter, @@ -64,7 +65,7 @@ export function FailedTransactionsCorrelations({ const { urlParams } = useUrlParams(); const { transactionName, start, end } = urlParams; - const displayLog = uiSettings.get(enableInspectEsQueries); + const inspectEnabled = uiSettings.get(enableInspectEsQueries); const searchServicePrams: SearchServiceParams = { environment, @@ -76,7 +77,7 @@ export function FailedTransactionsCorrelations({ end, }; - const result = useFailedTransactionsCorrelationsFetcher(searchServicePrams); + const result = useFailedTransactionsCorrelationsFetcher(); const { ccsWarning, @@ -87,10 +88,20 @@ export function FailedTransactionsCorrelations({ startFetch, cancelFetch, } = result; + + const startFetchHandler = useCallback(() => { + startFetch(searchServicePrams); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [environment, serviceName, kuery, start, end]); + // start fetching on load // we want this effect to execute exactly once after the component mounts useEffect(() => { - startFetch(); + if (isRunning) { + cancelFetch(); + } + + startFetchHandler(); return () => { // cancel any running async partial request when unmounting the component @@ -98,7 +109,7 @@ export function FailedTransactionsCorrelations({ cancelFetch(); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [startFetchHandler]); const [ selectedSignificantTerm, @@ -118,10 +129,83 @@ export function FailedTransactionsCorrelations({ const failedTransactionsCorrelationsColumns: Array< EuiBasicTableColumn - > = useMemo( - () => [ + > = useMemo(() => { + const percentageColumns: Array< + EuiBasicTableColumn + > = inspectEnabled + ? [ + { + width: '100px', + field: 'failurePercentage', + name: ( + + <> + {i18n.translate( + 'xpack.apm.correlations.failedTransactions.correlationsTable.failurePercentageLabel', + { + defaultMessage: 'Failure %', + } + )} + + + + ), + render: (failurePercentage: number) => + asPercent(failurePercentage, 1), + sortable: true, + }, + { + field: 'successPercentage', + width: '100px', + name: ( + + <> + {i18n.translate( + 'xpack.apm.correlations.failedTransactions.correlationsTable.successPercentageLabel', + { + defaultMessage: 'Success %', + } + )} + + + + ), + + render: (successPercentage: number) => + asPercent(successPercentage, 1), + sortable: true, + }, + ] + : []; + return [ { - width: '116px', + width: '80px', field: 'normalizedScore', name: ( <> @@ -140,6 +224,7 @@ export function FailedTransactionsCorrelations({ ); }, + sortable: true, }, { width: '116px', @@ -154,7 +239,13 @@ export function FailedTransactionsCorrelations({ )} ), - render: getFailedTransactionsCorrelationImpactLabel, + render: (pValue: number) => { + const label = getFailedTransactionsCorrelationImpactLabel(pValue); + return label ? ( + {label.impact} + ) : null; + }, + sortable: true, }, { field: 'fieldName', @@ -162,6 +253,7 @@ export function FailedTransactionsCorrelations({ 'xpack.apm.correlations.failedTransactions.correlationsTable.fieldNameLabel', { defaultMessage: 'Field name' } ), + sortable: true, }, { field: 'key', @@ -170,7 +262,9 @@ export function FailedTransactionsCorrelations({ { defaultMessage: 'Field value' } ), render: (fieldValue: string) => String(fieldValue).slice(0, 50), + sortable: true, }, + ...percentageColumns, { width: '100px', actions: [ @@ -188,9 +282,7 @@ export function FailedTransactionsCorrelations({ onClick: (term: FailedTransactionsCorrelationValue) => { push(history, { query: { - kuery: `${term.fieldName}:"${encodeURIComponent( - term.fieldValue - )}"`, + kuery: `${term.fieldName}:"${term.fieldValue}"`, }, }); onFilter(); @@ -211,9 +303,7 @@ export function FailedTransactionsCorrelations({ onClick: (term: FailedTransactionsCorrelationValue) => { push(history, { query: { - kuery: `not ${term.fieldName}:"${encodeURIComponent( - term.fieldValue - )}"`, + kuery: `not ${term.fieldName}:"${term.fieldValue}"`, }, }); onFilter(); @@ -231,9 +321,7 @@ export function FailedTransactionsCorrelations({ @@ -243,9 +331,7 @@ export function FailedTransactionsCorrelations({ @@ -255,9 +341,8 @@ export function FailedTransactionsCorrelations({ ); }, }, - ], - [history, onFilter, trackApmEvent] - ); + ] as Array>; + }, [history, onFilter, trackApmEvent, inspectEnabled]); useEffect(() => { if (isErrorMessage(error)) { @@ -273,100 +358,124 @@ export function FailedTransactionsCorrelations({ }); } }, [error, notifications.toasts]); + + const [sortField, setSortField] = useState< + keyof FailedTransactionsCorrelationValue + >('normalizedScore'); + const [sortDirection, setSortDirection] = useState('desc'); + + const onTableChange = useCallback(({ sort }) => { + const { field: currentSortField, direction: currentSortDirection } = sort; + + setSortField(currentSortField); + setSortDirection(currentSortDirection); + }, []); + + const { sorting, correlationTerms } = useMemo(() => { + if (!Array.isArray(result.values)) { + return { correlationTerms: [], sorting: undefined }; + } + const orderedTerms = orderBy( + result.values, + // The smaller the p value the higher the impact + // So we want to sort by the normalized score here + // which goes from 0 -> 1 + sortField === 'pValue' ? 'normalizedScore' : sortField, + sortDirection + ); + return { + correlationTerms: orderedTerms, + sorting: { + sort: { + field: sortField, + direction: sortDirection, + }, + } as EuiTableSortingType, + }; + }, [result?.values, sortField, sortDirection]); + return ( - <> - - - -
- {i18n.translate( - 'xpack.apm.correlations.failedTransactions.panelTitle', +
+ + + + +
+ {i18n.translate( + 'xpack.apm.correlations.failedTransactions.panelTitle', + { + defaultMessage: 'Failed transactions', + } + )} +
+
+
+ + + - - + title={i18n.translate( + 'xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaTitle', + { + defaultMessage: 'Failed transaction correlations', + } + )} + tooltipContent={i18n.translate( + 'xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsBetaDescription', + { + defaultMessage: + 'Failed transaction correlations is not GA. Please help us by reporting any bugs.', + } + )} + /> +
+ - + - + - - - {!isRunning && ( - - - - )} - {isRunning && ( - - - + + + + + {i18n.translate( + 'xpack.apm.correlations.failedTransactions.tableTitle', + { + defaultMessage: 'Correlations', + } )} - - - - - - - - - - - - - - - - - - {selectedTerm?.pValue != null ? ( + + + + + + + + {ccsWarning && ( + <> + + + + )} + + {inspectEnabled && + selectedTerm?.pValue != null && + (isRunning || correlationTerms.length > 0) ? ( <> {`p-value: ${selectedTerm.pValue.toPrecision(3)}`}, ]} /> - ) : null} - - columns={failedTransactionsCorrelationsColumns} - significantTerms={result?.values} - status={FETCH_STATUS.SUCCESS} - setSelectedSignificantTerm={setSelectedSignificantTerm} - selectedTerm={selectedTerm} - /> - {ccsWarning && ( - <> - - -

- {i18n.translate( - 'xpack.apm.correlations.failedTransactions.ccsWarningCalloutBody', - { - defaultMessage: - 'Data for the correlation analysis could not be fully retrieved. This feature is supported only for 7.15 and later versions.', - } - )} -

-
- - )} - {log.length > 0 && displayLog && ( - - - {log.map((d, i) => { - const splitItem = d.split(': '); - return ( -

- - {splitItem[0]} {splitItem[1]} - -

- ); - })} -
-
- )} - +
+ {(isRunning || correlationTerms.length > 0) && ( + + columns={failedTransactionsCorrelationsColumns} + significantTerms={correlationTerms} + status={isRunning ? FETCH_STATUS.LOADING : FETCH_STATUS.SUCCESS} + setSelectedSignificantTerm={setSelectedSignificantTerm} + selectedTerm={selectedTerm} + onTableChange={onTableChange} + sorting={sorting} + /> + )} + {correlationTerms.length < 1 && (progress === 1 || !isRunning) && ( + + )} +
+ {inspectEnabled && } +
); } diff --git a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations_help_popover.tsx b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations_help_popover.tsx index bebc889cc4ed9..e66101d619224 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations_help_popover.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations_help_popover.tsx @@ -18,6 +18,7 @@ export function FailedTransactionsCorrelationsHelpPopover() { anchorPosition="leftUp" button={ { setIsPopoverOpen((prevIsPopoverOpen) => !prevIsPopoverOpen); }} @@ -25,25 +26,28 @@ export function FailedTransactionsCorrelationsHelpPopover() { } closePopover={() => setIsPopoverOpen(false)} isOpen={isPopoverOpen} - title={i18n.translate('xpack.apm.correlations.failurePopoverTitle', { - defaultMessage: 'Failure correlations', - })} + title={i18n.translate( + 'xpack.apm.correlations.failedTransactions.helpPopover.title', + { + defaultMessage: 'Failed transaction correlations', + } + )} >

diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx index 6d6e56184e254..ed47d49948fba 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx @@ -8,24 +8,18 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { - EuiCallOut, - EuiCode, - EuiEmptyPrompt, - EuiAccordion, - EuiPanel, EuiIcon, EuiBasicTableColumn, - EuiButton, EuiFlexGroup, EuiFlexItem, - EuiProgress, EuiSpacer, - EuiText, EuiTitle, EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { Direction } from '@elastic/eui/src/services/sort/sort_direction'; +import { orderBy } from 'lodash'; +import { EuiTableSortingType } from '@elastic/eui/src/components/basic_table/table_types'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; @@ -42,6 +36,10 @@ import { useApmServiceContext } from '../../../context/apm_service/use_apm_servi import { LatencyCorrelationsHelpPopover } from './latency_correlations_help_popover'; import { useApmParams } from '../../../hooks/use_apm_params'; import { isErrorMessage } from './utils/is_error_message'; +import { CorrelationsLog } from './correlations_log'; +import { CorrelationsEmptyStatePrompt } from './empty_state_prompt'; +import { CrossClusterSearchCompatibilityWarning } from './cross_cluster_search_warning'; +import { CorrelationsProgressControls } from './progress_controls'; const DEFAULT_PERCENTILE_THRESHOLD = 95; @@ -133,15 +131,19 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { setSelectedSignificantTerm, ] = useState(null); - let selectedHistogram = histograms.length > 0 ? histograms[0] : undefined; + const selectedHistogram = useMemo(() => { + let selected = histograms.length > 0 ? histograms[0] : undefined; + + if (histograms.length > 0 && selectedSignificantTerm !== null) { + selected = histograms.find( + (h) => + h.field === selectedSignificantTerm.fieldName && + h.value === selectedSignificantTerm.fieldValue + ); + } + return selected; + }, [histograms, selectedSignificantTerm]); - if (histograms.length > 0 && selectedSignificantTerm !== null) { - selectedHistogram = histograms.find( - (h) => - h.field === selectedSignificantTerm.fieldName && - h.value === selectedSignificantTerm.fieldValue - ); - } const history = useHistory(); const trackApmEvent = useUiTracker({ app: 'apm' }); @@ -181,6 +183,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { render: (correlation: number) => { return
{asPreciseDecimal(correlation, 2)}
; }, + sortable: true, }, { field: 'fieldName', @@ -188,6 +191,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { 'xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldNameLabel', { defaultMessage: 'Field name' } ), + sortable: true, }, { field: 'fieldValue', @@ -196,6 +200,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { { defaultMessage: 'Field value' } ), render: (fieldValue: string) => String(fieldValue).slice(0, 50), + sortable: true, }, { width: '100px', @@ -214,9 +219,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { onClick: (term: MlCorrelationsTerms) => { push(history, { query: { - kuery: `${term.fieldName}:"${encodeURIComponent( - term.fieldValue - )}"`, + kuery: `${term.fieldName}:"${term.fieldValue}"`, }, }); onFilter(); @@ -237,9 +240,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { onClick: (term: MlCorrelationsTerms) => { push(history, { query: { - kuery: `not ${term.fieldName}:"${encodeURIComponent( - term.fieldValue - )}"`, + kuery: `not ${term.fieldName}:"${term.fieldValue}"`, }, }); onFilter(); @@ -256,17 +257,46 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { [history, onFilter, trackApmEvent] ); - const histogramTerms: MlCorrelationsTerms[] = useMemo(() => { - return histograms.map((d) => { - return { - fieldName: d.field, - fieldValue: d.value, - ksTest: d.ksTest, - correlation: d.correlation, - duplicatedFields: d.duplicatedFields, - }; - }); - }, [histograms]); + const [sortField, setSortField] = useState( + 'correlation' + ); + const [sortDirection, setSortDirection] = useState('desc'); + + const onTableChange = useCallback(({ sort }) => { + const { field: currentSortField, direction: currentSortDirection } = sort; + + setSortField(currentSortField); + setSortDirection(currentSortDirection); + }, []); + + const { histogramTerms, sorting } = useMemo(() => { + if (!Array.isArray(histograms)) { + return { histogramTerms: [], sorting: undefined }; + } + const orderedTerms = orderBy( + histograms.map((d) => { + return { + fieldName: d.field, + fieldValue: d.value, + ksTest: d.ksTest, + correlation: d.correlation, + duplicatedFields: d.duplicatedFields, + }; + }), + sortField, + sortDirection + ); + + return { + histogramTerms: orderedTerms, + sorting: { + sort: { + field: sortField, + direction: sortDirection, + }, + } as EuiTableSortingType, + }; + }, [histograms, sortField, sortDirection]); return (
@@ -300,88 +330,34 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { - +
{i18n.translate( 'xpack.apm.correlations.latencyCorrelations.tableTitle', { defaultMessage: 'Correlations', } )} - +
- - - - - - - - - - - - - - - {!isRunning && ( - - - - )} - {isRunning && ( - - - - )} - - + + {ccsWarning && ( <> - -

- {i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.ccsWarningCalloutBody', - { - defaultMessage: - 'Data for the correlation analysis could not be fully retrieved. This feature is supported only for 7.14 and later versions.', - } - )} -

-
+ )} + +
{(isRunning || histogramTerms.length > 0) && ( @@ -397,70 +373,15 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { } : undefined } + onTableChange={onTableChange} + sorting={sorting} /> )} {histogramTerms.length < 1 && (progress === 1 || !isRunning) && ( - <> - - -

- {i18n.translate( - 'xpack.apm.correlations.latencyCorrelations.noCorrelationsTitle', - { - defaultMessage: 'No significant correlations', - } - )} -

- - } - body={ - <> - - - - {/* Another EuiText element to enforce a line break */} - - - - - } - /> - + )}
- {log.length > 0 && displayLog && ( - - - {log.map((d, i) => { - const splitItem = d.split(': '); - return ( -

- - {splitItem[0]} {splitItem[1]} - -

- ); - })} -
-
- )} + {displayLog && }
); } diff --git a/x-pack/plugins/apm/public/components/app/correlations/progress_controls.tsx b/x-pack/plugins/apm/public/components/app/correlations/progress_controls.tsx new file mode 100644 index 0000000000000..a581313d6a5d5 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/progress_controls.tsx @@ -0,0 +1,77 @@ +/* + * 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 { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; + +export function CorrelationsProgressControls({ + progress, + onRefresh, + onCancel, + isRunning, +}: { + progress: number; + onRefresh: () => void; + onCancel: () => void; + isRunning: boolean; +}) { + return ( + + + + + + + + + + + + + + + {!isRunning && ( + + + + )} + {isRunning && ( + + + + )} + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts index d133ed1060ebe..edb7c8c16e267 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts @@ -8,6 +8,20 @@ import { getFailedTransactionsCorrelationImpactLabel } from './get_failed_transactions_correlation_impact_label'; import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/search_strategies/failure_correlations/constants'; +const EXPECTED_RESULT = { + HIGH: { + impact: FAILED_TRANSACTIONS_IMPACT_THRESHOLD.HIGH, + color: 'danger', + }, + MEDIUM: { + impact: FAILED_TRANSACTIONS_IMPACT_THRESHOLD.MEDIUM, + color: 'warning', + }, + LOW: { + impact: FAILED_TRANSACTIONS_IMPACT_THRESHOLD.LOW, + color: 'default', + }, +}; describe('getFailedTransactionsCorrelationImpactLabel', () => { it('returns null if value is invalid ', () => { expect(getFailedTransactionsCorrelationImpactLabel(-0.03)).toBe(null); @@ -21,32 +35,32 @@ describe('getFailedTransactionsCorrelationImpactLabel', () => { }); it('returns High if value is within [0, 1e-6) ', () => { - expect(getFailedTransactionsCorrelationImpactLabel(0)).toBe( - FAILED_TRANSACTIONS_IMPACT_THRESHOLD.HIGH + expect(getFailedTransactionsCorrelationImpactLabel(0)).toStrictEqual( + EXPECTED_RESULT.HIGH ); - expect(getFailedTransactionsCorrelationImpactLabel(1e-7)).toBe( - FAILED_TRANSACTIONS_IMPACT_THRESHOLD.HIGH + expect(getFailedTransactionsCorrelationImpactLabel(1e-7)).toStrictEqual( + EXPECTED_RESULT.HIGH ); }); it('returns Medium if value is within [1e-6, 1e-3) ', () => { - expect(getFailedTransactionsCorrelationImpactLabel(1e-6)).toBe( - FAILED_TRANSACTIONS_IMPACT_THRESHOLD.MEDIUM + expect(getFailedTransactionsCorrelationImpactLabel(1e-6)).toStrictEqual( + EXPECTED_RESULT.MEDIUM ); - expect(getFailedTransactionsCorrelationImpactLabel(1e-5)).toBe( - FAILED_TRANSACTIONS_IMPACT_THRESHOLD.MEDIUM + expect(getFailedTransactionsCorrelationImpactLabel(1e-5)).toStrictEqual( + EXPECTED_RESULT.MEDIUM ); - expect(getFailedTransactionsCorrelationImpactLabel(1e-4)).toBe( - FAILED_TRANSACTIONS_IMPACT_THRESHOLD.MEDIUM + expect(getFailedTransactionsCorrelationImpactLabel(1e-4)).toStrictEqual( + EXPECTED_RESULT.MEDIUM ); }); it('returns Low if value is within [1e-3, 0.02) ', () => { - expect(getFailedTransactionsCorrelationImpactLabel(1e-3)).toBe( - FAILED_TRANSACTIONS_IMPACT_THRESHOLD.LOW + expect(getFailedTransactionsCorrelationImpactLabel(1e-3)).toStrictEqual( + EXPECTED_RESULT.LOW ); - expect(getFailedTransactionsCorrelationImpactLabel(0.009)).toBe( - FAILED_TRANSACTIONS_IMPACT_THRESHOLD.LOW + expect(getFailedTransactionsCorrelationImpactLabel(0.009)).toStrictEqual( + EXPECTED_RESULT.LOW ); }); }); diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts index af64c50617019..5a806aba5371e 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts @@ -10,14 +10,23 @@ import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/sear export function getFailedTransactionsCorrelationImpactLabel( pValue: number -): FailureCorrelationImpactThreshold | null { +): { impact: FailureCorrelationImpactThreshold; color: string } | null { // The lower the p value, the higher the impact if (pValue >= 0 && pValue < 1e-6) - return FAILED_TRANSACTIONS_IMPACT_THRESHOLD.HIGH; + return { + impact: FAILED_TRANSACTIONS_IMPACT_THRESHOLD.HIGH, + color: 'danger', + }; if (pValue >= 1e-6 && pValue < 0.001) - return FAILED_TRANSACTIONS_IMPACT_THRESHOLD.MEDIUM; + return { + impact: FAILED_TRANSACTIONS_IMPACT_THRESHOLD.MEDIUM, + color: 'warning', + }; if (pValue >= 0.001 && pValue < 0.02) - return FAILED_TRANSACTIONS_IMPACT_THRESHOLD.LOW; + return { + impact: FAILED_TRANSACTIONS_IMPACT_THRESHOLD.LOW, + color: 'default', + }; return null; } diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx index 2506ac69f7aa2..86bef9917daf6 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx @@ -28,6 +28,9 @@ import { useApmParams } from '../../../../hooks/use_apm_params'; import { isErrorMessage } from '../../correlations/utils/is_error_message'; const DEFAULT_PERCENTILE_THRESHOLD = 95; +// Enforce min height so it's consistent across all tabs on the same level +// to prevent "flickering" behavior +const MIN_TAB_TITLE_HEIGHT = 56; type Selection = [number, number]; @@ -147,7 +150,7 @@ export function TransactionDistribution({ return (
- +
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx index 8743b8f3ea811..af66f818309a0 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/failed_transactions_correlations_tab.tsx @@ -46,7 +46,7 @@ function FailedTransactionsCorrelationsTab({ onFilter }: TabContentProps) { text={i18n.translate( 'xpack.apm.failedTransactionsCorrelations.licenseCheckText', { - defaultMessage: `To use the failed transactions correlations feature, you must be subscribed to an Elastic Platinum license.`, + defaultMessage: `To use the failed transaction correlations feature, you must be subscribed to an Elastic Platinum license. With it, you'll be able to discover which attributes are contributing to failed transactions.`, } )} /> diff --git a/x-pack/plugins/apm/public/hooks/use_failed_transactions_correlations_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_failed_transactions_correlations_fetcher.ts index 3841419e860fc..add00968f0444 100644 --- a/x-pack/plugins/apm/public/hooks/use_failed_transactions_correlations_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_failed_transactions_correlations_fetcher.ts @@ -38,9 +38,7 @@ interface FailedTransactionsCorrelationsFetcherState { total: number; } -export const useFailedTransactionsCorrelationsFetcher = ( - params: Omit -) => { +export const useFailedTransactionsCorrelationsFetcher = () => { const { services: { data }, } = useKibana(); @@ -74,7 +72,7 @@ export const useFailedTransactionsCorrelationsFetcher = ( })); } - const startFetch = () => { + const startFetch = (params: SearchServiceParams) => { setFetchState((prevState) => ({ ...prevState, error: undefined, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service.ts index 9afe9d916b38e..89fcda926d547 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service.ts @@ -15,6 +15,7 @@ import { fetchTransactionDurationFieldCandidates } from '../correlations/queries import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; import { fetchFailedTransactionsCorrelationPValues } from './queries/query_failure_correlation'; import { ERROR_CORRELATION_THRESHOLD } from './constants'; +import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; export const asyncErrorCorrelationSearchServiceProvider = ( esClient: ElasticsearchClient, @@ -35,10 +36,11 @@ export const asyncErrorCorrelationSearchServiceProvider = ( includeFrozen, }; - const { fieldCandidates } = await fetchTransactionDurationFieldCandidates( - esClient, - params - ); + const { + fieldCandidates: candidates, + } = await fetchTransactionDurationFieldCandidates(esClient, params); + + const fieldCandidates = candidates.filter((t) => !(t === EVENT_OUTCOME)); addLogMessage(`Identified ${fieldCandidates.length} fieldCandidates.`); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/queries/query_failure_correlation.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/queries/query_failure_correlation.ts index 22424d68f07ff..81fe6697d1fb1 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/queries/query_failure_correlation.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/queries/query_failure_correlation.ts @@ -75,14 +75,15 @@ export const fetchFailedTransactionsCorrelationPValues = async ( ); } - const result = (resp.body.aggregations - .failure_p_value as estypes.AggregationsMultiBucketAggregate<{ + const overallResult = resp.body.aggregations + .failure_p_value as estypes.AggregationsSignificantTermsAggregate<{ key: string; doc_count: number; bg_count: number; score: number; - }>).buckets.map((b) => { - const score = b.score; + }>; + const result = overallResult.buckets.map((bucket) => { + const score = bucket.score; // Scale the score into a value from 0 - 1 // using a concave piecewise linear function in -log(p-value) @@ -92,11 +93,17 @@ export const fetchFailedTransactionsCorrelationPValues = async ( 0.25 * Math.min(Math.max((score - 13.816) / 101.314, 0), 1); return { - ...b, + ...bucket, fieldName, - fieldValue: b.key, + fieldValue: bucket.key, pValue: Math.exp(-score), normalizedScore, + // Percentage of time the term appears in failed transactions + failurePercentage: bucket.doc_count / overallResult.doc_count, + // Percentage of time the term appears in successful transactions + successPercentage: + (bucket.bg_count - bucket.doc_count) / + (overallResult.bg_count - overallResult.doc_count), }; }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index aab8c658f0275..f02209bd7157e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5518,7 +5518,6 @@ "xpack.apm.correlations.customize.fieldPlaceholder": "オプションを選択または作成", "xpack.apm.correlations.customize.thresholdLabel": "しきい値", "xpack.apm.correlations.customize.thresholdPercentile": "{percentile}パーセンタイル", - "xpack.apm.correlations.latencyCorrelations.cancelButtonTitle": "キャンセル", "xpack.apm.correlations.latencyCorrelations.correlationsTable.actionsLabel": "フィルター", "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationColumnDescription": "サービスの遅延に対するフィールドの影響。0~1の範囲。", "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationLabel": "相関関係", @@ -5529,9 +5528,6 @@ "xpack.apm.correlations.latencyCorrelations.correlationsTable.filterDescription": "値でフィルタリング", "xpack.apm.correlations.latencyCorrelations.correlationsTable.filterLabel": "フィルター", "xpack.apm.correlations.latencyCorrelations.errorTitle": "相関関係の取得中にエラーが発生しました", - "xpack.apm.correlations.latencyCorrelations.progressAriaLabel": "進捗", - "xpack.apm.correlations.latencyCorrelations.progressTitle": "進捗状況: {progress}%", - "xpack.apm.correlations.latencyCorrelations.refreshButtonTitle": "更新", "xpack.apm.csm.breakdownFilter.browser": "ブラウザー", "xpack.apm.csm.breakdownFilter.device": "デバイス", "xpack.apm.csm.breakdownFilter.location": "場所", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 110ce52ea4e85..328d47f5d11dc 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5543,7 +5543,6 @@ "xpack.apm.correlations.customize.fieldPlaceholder": "选择或创建选项", "xpack.apm.correlations.customize.thresholdLabel": "阈值", "xpack.apm.correlations.customize.thresholdPercentile": "第 {percentile} 个百分位数", - "xpack.apm.correlations.latencyCorrelations.cancelButtonTitle": "取消", "xpack.apm.correlations.latencyCorrelations.correlationsTable.actionsLabel": "筛选", "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationColumnDescription": "字段对服务延迟的影响,范围从 0 到 1。", "xpack.apm.correlations.latencyCorrelations.correlationsTable.correlationLabel": "相关性", @@ -5554,9 +5553,6 @@ "xpack.apm.correlations.latencyCorrelations.correlationsTable.filterDescription": "按值筛选", "xpack.apm.correlations.latencyCorrelations.correlationsTable.filterLabel": "筛选", "xpack.apm.correlations.latencyCorrelations.errorTitle": "提取关联性时发生错误", - "xpack.apm.correlations.latencyCorrelations.progressAriaLabel": "进度", - "xpack.apm.correlations.latencyCorrelations.progressTitle": "进度:{progress}%", - "xpack.apm.correlations.latencyCorrelations.refreshButtonTitle": "刷新", "xpack.apm.csm.breakdownFilter.browser": "浏览器", "xpack.apm.csm.breakdownFilter.device": "设备", "xpack.apm.csm.breakdownFilter.location": "位置", diff --git a/x-pack/test/apm_api_integration/common/utils/parse_b_fetch.ts b/x-pack/test/apm_api_integration/common/utils/parse_b_fetch.ts new file mode 100644 index 0000000000000..79ea70f7199f9 --- /dev/null +++ b/x-pack/test/apm_api_integration/common/utils/parse_b_fetch.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import request from 'superagent'; + +export function parseBfetchResponse(resp: request.Response): Array> { + return resp.text + .trim() + .split('\n') + .map((item) => JSON.parse(item)); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts new file mode 100644 index 0000000000000..2d014b3ee1e6b --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts @@ -0,0 +1,238 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; +import { PartialSearchRequest } from '../../../../plugins/apm/server/lib/search_strategies/correlations/search_strategy'; +import { parseBfetchResponse } from '../../common/utils/parse_b_fetch'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const retry = getService('retry'); + const supertest = getService('supertest'); + + const getRequestBody = () => { + const partialSearchRequest: PartialSearchRequest = { + params: { + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + kuery: '', + }, + }; + + return { + batch: [ + { + request: partialSearchRequest, + options: { strategy: 'apmFailedTransactionsCorrelationsSearchStrategy' }, + }, + ], + }; + }; + + registry.when('on trial license without data', { config: 'trial', archives: [] }, () => { + it('queries the search strategy and returns results', async () => { + const intialResponse = await supertest + .post(`/internal/bsearch`) + .set('kbn-xsrf', 'foo') + .send(getRequestBody()); + + expect(intialResponse.status).to.eql( + 200, + `Expected status to be '200', got '${intialResponse.status}'` + ); + expect(intialResponse.body).to.eql( + {}, + `Expected response body to be an empty object, actual response is in the text attribute. Got: '${JSON.stringify( + intialResponse.body + )}'` + ); + + const body = parseBfetchResponse(intialResponse)[0]; + + expect(typeof body.result).to.be('object'); + const { result } = body; + + expect(typeof result?.id).to.be('string'); + + // pass on id for follow up queries + const searchStrategyId = result.id; + + // follow up request body including search strategy ID + const reqBody = getRequestBody(); + reqBody.batch[0].request.id = searchStrategyId; + + let followUpResponse: Record = {}; + + // continues querying until the search strategy finishes + await retry.waitForWithTimeout( + 'search strategy eventually completes and returns full results', + 5000, + async () => { + const response = await supertest + .post(`/internal/bsearch`) + .set('kbn-xsrf', 'foo') + .send(reqBody); + + followUpResponse = parseBfetchResponse(response)[0]; + + return ( + followUpResponse?.result?.isRunning === false || followUpResponse?.error !== undefined + ); + } + ); + + expect(followUpResponse?.error).to.eql( + undefined, + `search strategy should not return an error, got: ${JSON.stringify( + followUpResponse?.error + )}` + ); + + const followUpResult = followUpResponse.result; + expect(followUpResult?.isRunning).to.eql(false, 'search strategy should not be running'); + expect(followUpResult?.isPartial).to.eql( + false, + 'search strategy result should not be partial' + ); + expect(followUpResult?.id).to.eql( + searchStrategyId, + 'search strategy id should match original id' + ); + expect(followUpResult?.isRestored).to.eql( + true, + 'search strategy response should be restored' + ); + expect(followUpResult?.loaded).to.eql(100, 'loaded state should be 100'); + expect(followUpResult?.total).to.eql(100, 'total state should be 100'); + + expect(typeof followUpResult?.rawResponse).to.be('object'); + + const { rawResponse: finalRawResponse } = followUpResult; + + expect(typeof finalRawResponse?.took).to.be('number'); + + expect(finalRawResponse?.values.length).to.eql( + 0, + `Expected 0 identified correlations, got ${finalRawResponse?.values.length}.` + ); + }); + }); + + registry.when('on trial license with data', { config: 'trial', archives: ['8.0.0'] }, () => { + it('queries the search strategy and returns results', async () => { + const intialResponse = await supertest + .post(`/internal/bsearch`) + .set('kbn-xsrf', 'foo') + .send(getRequestBody()); + + expect(intialResponse.status).to.eql( + 200, + `Expected status to be '200', got '${intialResponse.status}'` + ); + expect(intialResponse.body).to.eql( + {}, + `Expected response body to be an empty object, actual response is in the text attribute. Got: '${JSON.stringify( + intialResponse.body + )}'` + ); + + const body = parseBfetchResponse(intialResponse)[0]; + + expect(typeof body.result).to.be('object'); + const { result } = body; + + expect(typeof result?.id).to.be('string'); + + // pass on id for follow up queries + const searchStrategyId = result.id; + + // follow up request body including search strategy ID + const reqBody = getRequestBody(); + reqBody.batch[0].request.id = searchStrategyId; + + let followUpResponse: Record = {}; + + // continues querying until the search strategy finishes + await retry.waitForWithTimeout( + 'search strategy eventually completes and returns full results', + 5000, + async () => { + const response = await supertest + .post(`/internal/bsearch`) + .set('kbn-xsrf', 'foo') + .send(reqBody); + + followUpResponse = parseBfetchResponse(response)[0]; + + return ( + followUpResponse?.result?.isRunning === false || followUpResponse?.error !== undefined + ); + } + ); + + expect(followUpResponse?.error).to.eql( + undefined, + `search strategy should not return an error, got: ${JSON.stringify( + followUpResponse?.error + )}` + ); + + const followUpResult = followUpResponse.result; + expect(followUpResult?.isRunning).to.eql(false, 'search strategy should not be running'); + expect(followUpResult?.isPartial).to.eql( + false, + 'search strategy result should not be partial' + ); + expect(followUpResult?.id).to.eql( + searchStrategyId, + 'search strategy id should match original id' + ); + expect(followUpResult?.isRestored).to.eql( + true, + 'search strategy response should be restored' + ); + expect(followUpResult?.loaded).to.eql(100, 'loaded state should be 100'); + expect(followUpResult?.total).to.eql(100, 'total state should be 100'); + + expect(typeof followUpResult?.rawResponse).to.be('object'); + + const { rawResponse: finalRawResponse } = followUpResult; + + expect(typeof finalRawResponse?.took).to.be('number'); + expect(finalRawResponse?.percentileThresholdValue).to.be(undefined); + expect(finalRawResponse?.overallHistogram).to.be(undefined); + + expect(finalRawResponse?.values.length).to.eql( + 43, + `Expected 43 identified correlations, got ${finalRawResponse?.values.length}.` + ); + + expect(finalRawResponse?.log.map((d: string) => d.split(': ')[1])).to.eql([ + 'Identified 68 fieldCandidates.', + 'Identified correlations for 68 fields out of 68 candidates.', + 'Identified 43 significant correlations relating to failed transactions.', + ]); + + const sortedCorrelations = finalRawResponse?.values.sort(); + const correlation = sortedCorrelations[0]; + + expect(typeof correlation).to.be('object'); + expect(correlation?.key).to.be('HTTP 5xx'); + expect(correlation?.doc_count).to.be(31); + expect(correlation?.score).to.be(100.17736139032642); + expect(correlation?.bg_count).to.be(60); + expect(correlation?.fieldName).to.be('transaction.result'); + expect(correlation?.fieldValue).to.be('HTTP 5xx'); + expect(typeof correlation?.pValue).to.be('number'); + expect(typeof correlation?.normalizedScore).to.be('number'); + expect(typeof correlation?.failurePercentage).to.be('number'); + expect(typeof correlation?.successPercentage).to.be('number'); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts b/x-pack/test/apm_api_integration/tests/correlations/latency.ts similarity index 97% rename from x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts rename to x-pack/test/apm_api_integration/tests/correlations/latency.ts index e41a830735a89..32ca71694626f 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/latency.ts @@ -6,18 +6,10 @@ */ import expect from '@kbn/expect'; -import request from 'superagent'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; - import { PartialSearchRequest } from '../../../../plugins/apm/server/lib/search_strategies/correlations/search_strategy'; - -function parseBfetchResponse(resp: request.Response): Array> { - return resp.text - .trim() - .split('\n') - .map((item) => JSON.parse(item)); -} +import { parseBfetchResponse } from '../../common/utils/parse_b_fetch'; export default function ApiTest({ getService }: FtrProviderContext) { const retry = getService('retry'); diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 0e76a4ed86688..c8a57bc613a92 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -28,12 +28,17 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./alerts/rule_registry')); }); + // correlations describe('correlations/latency_slow_transactions', function () { loadTestFile(require.resolve('./correlations/latency_slow_transactions')); }); - describe('correlations/latency_ml', function () { - loadTestFile(require.resolve('./correlations/latency_ml')); + describe('correlations/failed_transactions', function () { + loadTestFile(require.resolve('./correlations/failed_transactions')); + }); + + describe('correlations/latency', function () { + loadTestFile(require.resolve('./correlations/latency')); }); describe('correlations/latency_overall', function () { diff --git a/x-pack/test/functional/apps/apm/correlations/failed_transaction_correlations.ts b/x-pack/test/functional/apps/apm/correlations/failed_transaction_correlations.ts new file mode 100644 index 0000000000000..969a543e5f97d --- /dev/null +++ b/x-pack/test/functional/apps/apm/correlations/failed_transaction_correlations.ts @@ -0,0 +1,155 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const find = getService('find'); + const retry = getService('retry'); + const spacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + const testData = { + correlationsTab: 'Failed transaction correlations', + serviceName: 'opbeans-go', + transactionsTab: 'Transactions', + transaction: 'GET /api/stats', + }; + + describe('failed transactions correlations', () => { + describe('space with no features disabled', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/infra/8.0.0/metrics_and_apm'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it('shows apm navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map((link) => link.text); + expect(navLinks).to.contain('APM'); + }); + + it('can navigate to APM app', async () => { + await PageObjects.common.navigateToApp('apm'); + + await retry.try(async () => { + await testSubjects.existOrFail('apmMainContainer', { + timeout: 10000, + }); + + const apmMainContainerText = await testSubjects.getVisibleTextAll('apmMainContainer'); + const apmMainContainerTextItems = apmMainContainerText[0].split('\n'); + expect(apmMainContainerTextItems).to.contain('No services found'); + }); + }); + + it('sets the timePicker to return data', async () => { + await PageObjects.timePicker.timePickerExists(); + + const fromTime = 'Jul 29, 2019 @ 00:00:00.000'; + const toTime = 'Jul 30, 2019 @ 00:00:00.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + + await retry.try(async () => { + const apmMainContainerText = await testSubjects.getVisibleTextAll('apmMainContainer'); + const apmMainContainerTextItems = apmMainContainerText[0].split('\n'); + + expect(apmMainContainerTextItems).to.not.contain('No services found'); + + expect(apmMainContainerTextItems).to.contain('opbeans-go'); + expect(apmMainContainerTextItems).to.contain('opbeans-node'); + expect(apmMainContainerTextItems).to.contain('opbeans-ruby'); + expect(apmMainContainerTextItems).to.contain('opbeans-python'); + expect(apmMainContainerTextItems).to.contain('opbeans-dotnet'); + expect(apmMainContainerTextItems).to.contain('opbeans-java'); + + expect(apmMainContainerTextItems).to.contain('development'); + + const items = await testSubjects.findAll('apmServiceListAppLink'); + expect(items.length).to.be(6); + }); + }); + + it(`navigates to the 'opbeans-go' service overview page`, async function () { + await find.clickByDisplayedLinkText(testData.serviceName); + + await retry.try(async () => { + const apmMainTemplateHeaderServiceName = await testSubjects.getVisibleTextAll( + 'apmMainTemplateHeaderServiceName' + ); + expect(apmMainTemplateHeaderServiceName).to.contain(testData.serviceName); + }); + }); + + it('navigates to the transactions tab', async function () { + await find.clickByDisplayedLinkText(testData.transactionsTab); + + await retry.try(async () => { + const apmMainContainerText = await testSubjects.getVisibleTextAll('apmMainContainer'); + const apmMainContainerTextItems = apmMainContainerText[0].split('\n'); + + expect(apmMainContainerTextItems).to.contain(testData.transaction); + }); + }); + + it(`navigates to the 'GET /api/stats' transactions`, async function () { + await find.clickByDisplayedLinkText(testData.transaction); + + await retry.try(async () => { + const apmMainContainerText = await testSubjects.getVisibleTextAll('apmMainContainer'); + const apmMainContainerTextItems = apmMainContainerText[0].split('\n'); + + expect(apmMainContainerTextItems).to.contain(testData.transaction); + expect(apmMainContainerTextItems).to.contain(testData.correlationsTab); + }); + }); + + it('shows the failed transactions correlations tab', async function () { + await testSubjects.click('apmFailedTransactionsCorrelationsTabButton'); + + await retry.try(async () => { + await testSubjects.existOrFail('apmFailedTransactionsCorrelationsTabContent'); + }); + }); + + it('loads the failed transactions correlations results', async function () { + await retry.try(async () => { + const apmFailedTransactionsCorrelationsTabTitle = await testSubjects.getVisibleText( + 'apmFailedTransactionsCorrelationsTabTitle' + ); + expect(apmFailedTransactionsCorrelationsTabTitle).to.be('Failed transactions'); + + // Assert that the data fully loaded to 100% + const apmFailedTransactionsCorrelationsProgressTitle = await testSubjects.getVisibleText( + 'apmCorrelationsProgressTitle' + ); + expect(apmFailedTransactionsCorrelationsProgressTitle).to.be('Progress: 100%'); + + // Assert that results for the given service didn't find any correlations + const apmCorrelationsTable = await testSubjects.getVisibleText('apmCorrelationsTable'); + expect(apmCorrelationsTable).to.be( + 'No significant correlations\nCorrelations will only be identified if they have significant impact.\nTry selecting another time range or remove any added filter.' + ); + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/apm/correlations/index.ts b/x-pack/test/functional/apps/apm/correlations/index.ts index ae5f594e54400..abf74910f4e8d 100644 --- a/x-pack/test/functional/apps/apm/correlations/index.ts +++ b/x-pack/test/functional/apps/apm/correlations/index.ts @@ -9,7 +9,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('correlations', function () { - this.tags('skipFirefox'); + this.tags(['skipFirefox', 'apm']); loadTestFile(require.resolve('./latency_correlations')); + loadTestFile(require.resolve('./failed_transaction_correlations')); }); } diff --git a/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts b/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts index 3f743c378b7c3..af3633798133b 100644 --- a/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts +++ b/x-pack/test/functional/apps/apm/correlations/latency_correlations.ts @@ -144,10 +144,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('loads the correlation results', async function () { await retry.try(async () => { // Assert that the data fully loaded to 100% - const apmCorrelationsLatencyCorrelationsProgressTitle = await testSubjects.getVisibleText( - 'apmCorrelationsLatencyCorrelationsProgressTitle' + const apmLatencyCorrelationsProgressTitle = await testSubjects.getVisibleText( + 'apmCorrelationsProgressTitle' ); - expect(apmCorrelationsLatencyCorrelationsProgressTitle).to.be('Progress: 100%'); + expect(apmLatencyCorrelationsProgressTitle).to.be('Progress: 100%'); // Assert that the Correlations Chart and its header are present const apmCorrelationsLatencyCorrelationsChartTitle = await testSubjects.getVisibleText(