diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 5959eb6aca4d4..41bb7c07bda7e 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -412,4 +412,8 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'observability:enableInspectEsQueries': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, }; diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index fd63bb5bcaf43..c4a70f5065d8e 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -31,6 +31,7 @@ export interface UsageStats { 'apm:enableSignificantTerms': boolean; 'apm:enableServiceOverview': boolean; 'observability:enableAlertingExperience': boolean; + 'observability:enableInspectEsQueries': boolean; 'visualize:enableLabs': boolean; 'visualization:heatmap:maxBuckets': number; 'visualization:colorMapping': string; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 451b3ffe91535..3cc054bdcac88 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -8032,6 +8032,12 @@ "_meta": { "description": "Non-default value of setting." } + }, + "observability:enableInspectEsQueries": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } } } }, diff --git a/x-pack/plugins/apm/common/apm_api/parse_endpoint.ts b/x-pack/plugins/apm/common/apm_api/parse_endpoint.ts new file mode 100644 index 0000000000000..fb7ef6d36ce25 --- /dev/null +++ b/x-pack/plugins/apm/common/apm_api/parse_endpoint.ts @@ -0,0 +1,32 @@ +/* + * 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. + */ + +type Method = 'get' | 'post' | 'put' | 'delete'; + +export function parseEndpoint( + endpoint: string, + pathParams: Record = {} +) { + const [method, rawPathname] = endpoint.split(' '); + + // replace template variables with path params + const pathname = Object.keys(pathParams).reduce((acc, paramName) => { + return acc.replace(`{${paramName}}`, pathParams[paramName]); + }, rawPathname); + + return { method: parseMethod(method), pathname }; +} + +export function parseMethod(method: string) { + const res = method.trim().toLowerCase() as Method; + + if (!['get', 'post', 'put', 'delete'].includes(res)) { + throw new Error('Endpoint was not prefixed with a valid HTTP method'); + } + + return res; +} diff --git a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts b/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts index 3316c74d52e38..4212e0430ff5f 100644 --- a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts +++ b/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts @@ -45,10 +45,10 @@ describe('strictKeysRt', () => { { type: t.intersection([ t.type({ query: t.type({ bar: t.string }) }), - t.partial({ query: t.partial({ _debug: t.boolean }) }), + t.partial({ query: t.partial({ _inspect: t.boolean }) }), ]), - passes: [{ query: { bar: '', _debug: true } }], - fails: [{ query: { _debug: true } }], + passes: [{ query: { bar: '', _inspect: true } }], + fails: [{ query: { _inspect: true } }], }, ]; @@ -91,12 +91,12 @@ describe('strictKeysRt', () => { } as Record); const typeB = t.partial({ - query: t.partial({ _debug: jsonRt.pipe(t.boolean) }), + query: t.partial({ _inspect: jsonRt.pipe(t.boolean) }), }); const value = { query: { - _debug: 'true', + _inspect: 'true', filterNames: JSON.stringify(['host', 'agentName']), }, }; diff --git a/x-pack/plugins/apm/public/application/application.test.tsx b/x-pack/plugins/apm/public/application/application.test.tsx index b785fcc7dab08..7df6ca343426c 100644 --- a/x-pack/plugins/apm/public/application/application.test.tsx +++ b/x-pack/plugins/apm/public/application/application.test.tsx @@ -8,7 +8,7 @@ import { act } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import { Observable } from 'rxjs'; -import { AppMountParameters, CoreStart, HttpSetup } from 'src/core/public'; +import { AppMountParameters, CoreStart } from 'src/core/public'; import { mockApmPluginContextValue } from '../context/apm_plugin/mock_apm_plugin_context'; import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; @@ -72,7 +72,7 @@ describe('renderApp', () => { embeddable, }; jest.spyOn(window, 'scrollTo').mockReturnValueOnce(undefined); - createCallApmApi((core.http as unknown) as HttpSetup); + createCallApmApi((core as unknown) as CoreStart); jest .spyOn(window.console, 'warn') diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index 8ea4593bb89a7..787b15d0a5675 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -118,7 +118,7 @@ export const renderApp = ( ) => { const { element } = appMountParameters; - createCallApmApi(core.http); + createCallApmApi(core); // Automatically creates static index pattern and stores as saved object createStaticIndexPattern().catch((e) => { diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index 64600dd500bd5..bc14bc1531686 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -120,7 +120,7 @@ export const renderApp = ( // render APM feedback link in global help menu setHelpExtension(core); setReadonlyBadge(core); - createCallApmApi(core.http); + createCallApmApi(core); // Automatically creates static index pattern and stores as saved object createStaticIndexPattern().catch((e) => { diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx index 29f74b26d310c..fdfed6eb0d685 100644 --- a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx @@ -107,7 +107,11 @@ export function ErrorCountAlertTrigger(props: Props) { ]; const chartPreview = ( - + ); return ( diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx index 11aab788ec7f4..b4c78b54f329b 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx @@ -13,7 +13,6 @@ import { useParams } from 'react-router-dom'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { getDurationFormatter } from '../../../../common/utils/formatters'; -import { TimeSeries } from '../../../../typings/timeseries'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; @@ -116,9 +115,9 @@ export function TransactionDurationAlertTrigger(props: Props) { ] ); - const maxY = getMaxY([ - { data: data ?? [] } as TimeSeries<{ x: number; y: number | null }>, - ]); + const latencyChartPreview = data?.latencyChartPreview ?? []; + + const maxY = getMaxY([{ data: latencyChartPreview }]); const formatter = getDurationFormatter(maxY); const yTickFormat = getResponseTimeTickFormatter(formatter); @@ -127,7 +126,7 @@ export function TransactionDurationAlertTrigger(props: Props) { const chartPreview = ( diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx index de30af4a4707f..c6f9c4efd98b6 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx @@ -132,7 +132,7 @@ export function TransactionErrorRateAlertTrigger(props: Props) { const chartPreview = ( asPercent(d, 1)} threshold={thresholdAsPercent} /> diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx index 6c94b895f6924..db5932a96fb12 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx @@ -35,7 +35,7 @@ export function BreakdownSeries({ ? EUI_CHARTS_THEME_DARK : EUI_CHARTS_THEME_LIGHT; - const { data, status } = useBreakdowns({ + const { breakdowns, status } = useBreakdowns({ field, value, percentileRange, @@ -49,7 +49,7 @@ export function BreakdownSeries({ // so don't user that here return ( <> - {data?.map(({ data: seriesData, name }, sortIndex) => ( + {breakdowns.map(({ data: seriesData, name }, sortIndex) => ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts index 5af7f0682db19..e21aaa08c432d 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts @@ -17,12 +17,10 @@ interface Props { export const useBreakdowns = ({ percentileRange, field, value }: Props) => { const { urlParams, uiFilters } = useUrlParams(); - const { start, end, searchTerm } = urlParams; - const { min: minP, max: maxP } = percentileRange ?? {}; - return useFetcher( + const { data, status } = useFetcher( (callApmApi) => { if (start && end && field && value) { return callApmApi({ @@ -47,4 +45,6 @@ export const useBreakdowns = ({ percentileRange, field, value }: Props) => { }, [end, start, uiFilters, field, value, minP, maxP, searchTerm] ); + + return { breakdowns: data?.pageLoadDistBreakdown ?? [], status }; }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx index e3e2a979c48d3..d04bcb79a53e1 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx @@ -38,6 +38,7 @@ export function MainFilters() { [start, end] ); + const rumServiceNames = data?.rumServices ?? []; const { isSmall } = useBreakPoints(); // on mobile we want it to take full width @@ -48,7 +49,7 @@ export function MainFilters() { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts index c40f6ba2b8850..8ae4c9dc0e01d 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts @@ -68,7 +68,7 @@ export function useLocalUIFilters({ }); }; - const { data = getInitialData(filterNames), status } = useFetcher( + const { data, status } = useFetcher( (callApmApi) => { if (shouldFetch && urlParams.start && urlParams.end) { return callApmApi({ @@ -96,7 +96,8 @@ export function useLocalUIFilters({ ] ); - const filters = data.map((filter) => ({ + const localUiFilters = data?.localUiFilters ?? getInitialData(filterNames); + const filters = localUiFilters.map((filter) => ({ ...filter, value: values[filter.name] || [], })); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/use_call_api.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/use_call_api.ts index 5b448871804eb..f932cec3cacb6 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/use_call_api.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/use_call_api.ts @@ -11,9 +11,9 @@ import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plug import { FetchOptions } from '../../../../../common/fetch_options'; export function useCallApi() { - const { http } = useApmPluginContext().core; + const { core } = useApmPluginContext(); return useMemo(() => { - return (options: FetchOptions) => callApi(http, options); - }, [http]); + return (options: FetchOptions) => callApi(core, options); + }, [core]); } diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx index d754710dc84fa..ac1846155569a 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -6,7 +6,7 @@ */ import cytoscape from 'cytoscape'; -import { HttpSetup } from 'kibana/public'; +import { CoreStart } from 'kibana/public'; import React, { ComponentType } from 'react'; import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; @@ -21,19 +21,21 @@ export default { component: Popover, decorators: [ (Story: ComponentType) => { - const httpMock = ({ - get: async () => ({ - avgCpuUsage: 0.32809666568309237, - avgErrorRate: 0.556068173242986, - avgMemoryUsage: 0.5504868173242986, - transactionStats: { - avgRequestsPerMinute: 164.47222031860858, - avgTransactionDuration: 61634.38905590272, - }, - }), - } as unknown) as HttpSetup; + const coreMock = ({ + http: { + get: async () => ({ + avgCpuUsage: 0.32809666568309237, + avgErrorRate: 0.556068173242986, + avgMemoryUsage: 0.5504868173242986, + transactionStats: { + avgRequestsPerMinute: 164.47222031860858, + avgTransactionDuration: 61634.38905590272, + }, + }), + }, + } as unknown) as CoreStart; - createCallApmApi(httpMock); + createCallApmApi(coreMock); return ( diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx index e762f517ce1b5..71355a84d28d4 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx @@ -33,7 +33,7 @@ interface Props { } export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) { - const { data: serviceNames = [], status: serviceNamesStatus } = useFetcher( + const { data: serviceNamesData, status: serviceNamesStatus } = useFetcher( (callApmApi) => { return callApmApi({ endpoint: 'GET /api/apm/settings/agent-configuration/services', @@ -43,8 +43,9 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) { [], { preservePreviousData: false } ); + const serviceNames = serviceNamesData?.serviceNames ?? []; - const { data: environments = [], status: environmentStatus } = useFetcher( + const { data: environmentsData, status: environmentsStatus } = useFetcher( (callApmApi) => { if (newConfig.service.name) { return callApmApi({ @@ -59,6 +60,8 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) { { preservePreviousData: false } ); + const environments = environmentsData?.environments ?? []; + const { status: agentNameStatus } = useFetcher( async (callApmApi) => { const serviceName = newConfig.service.name; @@ -153,11 +156,11 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) { 'xpack.apm.agentConfig.servicePage.environment.fieldLabel', { defaultMessage: 'Service environment' } )} - isLoading={environmentStatus === FETCH_STATUS.LOADING} + isLoading={environmentsStatus === FETCH_STATUS.LOADING} options={environmentOptions} value={newConfig.service.environment} disabled={ - !newConfig.service.name || environmentStatus === FETCH_STATUS.LOADING + !newConfig.service.name || environmentsStatus === FETCH_STATUS.LOADING } onChange={(e) => { e.preventDefault(); diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx index 4d2754a677bf7..cd5fa5db89a31 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx @@ -7,7 +7,7 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; -import { HttpSetup } from 'kibana/public'; +import { CoreStart } from 'kibana/public'; import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; import { AgentConfiguration } from '../../../../../../common/agent_configuration/configuration_types'; import { FETCH_STATUS } from '../../../../../hooks/use_fetcher'; @@ -23,10 +23,10 @@ storiesOf( module ) .addDecorator((storyFn) => { - const httpMock = {}; + const coreMock = ({} as unknown) as CoreStart; // mock - createCallApmApi((httpMock as unknown) as HttpSetup); + createCallApmApi(coreMock); const contextMock = { core: { diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx index 081a3dbc907c5..3e3bc892e6518 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx @@ -16,7 +16,7 @@ import { } from '../../../../../services/rest/createCallApmApi'; import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context'; -type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>[0]; +type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>['configurations'][0]; interface Props { config: Config; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx index bef0dfc22280c..c098be41968dd 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx @@ -32,15 +32,19 @@ import { ITableColumn, ManagedTable } from '../../../../shared/ManagedTable'; import { TimestampTooltip } from '../../../../shared/TimestampTooltip'; import { ConfirmDeleteModal } from './ConfirmDeleteModal'; -type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>[0]; +type Config = APIReturnType<'GET /api/apm/settings/agent-configuration'>['configurations'][0]; interface Props { status: FETCH_STATUS; - data: Config[]; + configurations: Config[]; refetch: () => void; } -export function AgentConfigurationList({ status, data, refetch }: Props) { +export function AgentConfigurationList({ + status, + configurations, + refetch, +}: Props) { const { core } = useApmPluginContext(); const canSave = core.application.capabilities.apm.save; const { basePath } = core.http; @@ -113,7 +117,7 @@ export function AgentConfigurationList({ status, data, refetch }: Props) { return failurePrompt; } - if (status === FETCH_STATUS.SUCCESS && isEmpty(data)) { + if (status === FETCH_STATUS.SUCCESS && isEmpty(configurations)) { return emptyStatePrompt; } @@ -231,7 +235,7 @@ export function AgentConfigurationList({ status, data, refetch }: Props) { } columns={columns} - items={data} + items={configurations} initialSortField="service.name" initialSortDirection="asc" initialPageSize={20} diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx index 8aa0c35f36717..3225951fd6c70 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx @@ -25,8 +25,10 @@ import { useFetcher } from '../../../../hooks/use_fetcher'; import { createAgentConfigurationHref } from '../../../shared/Links/apm/agentConfigurationLinks'; import { AgentConfigurationList } from './List'; +const INITIAL_DATA = { configurations: [] }; + export function AgentConfigurations() { - const { refetch, data = [], status } = useFetcher( + const { refetch, data = INITIAL_DATA, status } = useFetcher( (callApmApi) => callApmApi({ endpoint: 'GET /api/apm/settings/agent-configuration' }), [], @@ -36,7 +38,7 @@ export function AgentConfigurations() { useTrackPageview({ app: 'apm', path: 'agent_configuration' }); useTrackPageview({ app: 'apm', path: 'agent_configuration', delay: 15000 }); - const hasConfigurations = !isEmpty(data); + const hasConfigurations = !isEmpty(data.configurations); return ( <> @@ -72,7 +74,11 @@ export function AgentConfigurations() { - + ); diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx index 9722c99990e3f..9d2b4bba22afb 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx @@ -24,7 +24,10 @@ import React, { useEffect, useState } from 'react'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { clearCache } from '../../../../services/rest/callApi'; -import { callApmApi } from '../../../../services/rest/createCallApmApi'; +import { + APIReturnType, + callApmApi, +} from '../../../../services/rest/createCallApmApi'; const APM_INDEX_LABELS = [ { @@ -84,8 +87,10 @@ async function saveApmIndices({ clearCache(); } +type ApiResponse = APIReturnType<`GET /api/apm/settings/apm-index-settings`>; + // avoid infinite loop by initializing the state outside the component -const INITIAL_STATE = [] as []; +const INITIAL_STATE: ApiResponse = { apmIndexSettings: [] }; export function ApmIndices() { const { core } = useApmPluginContext(); @@ -108,7 +113,7 @@ export function ApmIndices() { useEffect(() => { setApmIndices( - data.reduce( + data.apmIndexSettings.reduce( (acc, { configurationName, savedValue }) => ({ ...acc, [configurationName]: savedValue, @@ -190,7 +195,7 @@ export function ApmIndices() { {APM_INDEX_LABELS.map(({ configurationName, label }) => { - const matchedConfiguration = data.find( + const matchedConfiguration = data.apmIndexSettings.find( ({ configurationName: configName }) => configName === configurationName ); diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx index 0dbc8f6235342..77835afef863a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx @@ -24,20 +24,12 @@ import { } from '../../../../../utils/testHelpers'; import * as saveCustomLink from './CreateEditCustomLinkFlyout/saveCustomLink'; -const data = [ - { - id: '1', - label: 'label 1', - url: 'url 1', - 'service.name': 'opbeans-java', - }, - { - id: '2', - label: 'label 2', - url: 'url 2', - 'transaction.type': 'request', - }, -]; +const data = { + customLinks: [ + { id: '1', label: 'label 1', url: 'url 1', 'service.name': 'opbeans-java' }, + { id: '2', label: 'label 2', url: 'url 2', 'transaction.type': 'request' }, + ], +}; function getMockAPMContext({ canSave }: { canSave: boolean }) { return ({ @@ -69,7 +61,7 @@ describe('CustomLink', () => { describe('empty prompt', () => { beforeAll(() => { jest.spyOn(hooks, 'useFetcher').mockReturnValue({ - data: [], + data: { customLinks: [] }, status: hooks.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); @@ -290,7 +282,7 @@ describe('CustomLink', () => { describe('invalid license', () => { beforeAll(() => { jest.spyOn(hooks, 'useFetcher').mockReturnValue({ - data: [], + data: { customLinks: [] }, status: hooks.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index 4b4bc2e8feeab..49fa3eab47862 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -35,7 +35,7 @@ export function CustomLinkOverview() { CustomLink | undefined >(); - const { data: customLinks = [], status, refetch } = useFetcher( + const { data, status, refetch } = useFetcher( async (callApmApi) => { if (hasValidLicense) { return callApmApi({ @@ -46,6 +46,8 @@ export function CustomLinkOverview() { [hasValidLicense] ); + const customLinks = data?.customLinks ?? []; + useEffect(() => { if (customLinkSelected) { setIsFlyoutOpen(true); diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx index 6a11f862994e2..bf9062418313a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx @@ -21,6 +21,7 @@ import { EuiEmptyPrompt, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { ML_ERRORS } from '../../../../../common/anomaly_detection'; import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; @@ -33,6 +34,10 @@ interface Props { onCreateJobSuccess: () => void; onCancel: () => void; } + +type ApiResponse = APIReturnType<'GET /api/apm/settings/anomaly-detection/environments'>; +const INITIAL_DATA: ApiResponse = { environments: [] }; + export function AddEnvironments({ currentEnvironments, onCreateJobSuccess, @@ -42,7 +47,7 @@ export function AddEnvironments({ const { anomalyDetectionJobsRefetch } = useAnomalyDetectionJobsContext(); const canCreateJob = !!application.capabilities.ml.canCreateJob; const { toasts } = notifications; - const { data = [], status } = useFetcher( + const { data = INITIAL_DATA, status } = useFetcher( (callApmApi) => callApmApi({ endpoint: `GET /api/apm/settings/anomaly-detection/environments`, @@ -51,7 +56,7 @@ export function AddEnvironments({ { preservePreviousData: false } ); - const environmentOptions = data.map((env) => ({ + const environmentOptions = data.environments.map((env) => ({ label: getEnvironmentLabel(env), value: env, disabled: currentEnvironments.includes(env), diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx index 66fb72975acea..f31354bc7aa3c 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx @@ -49,10 +49,10 @@ const Culprit = euiStyled.div` font-family: ${fontFamilyCode}; `; -type ErrorGroupListAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/errors'>; +type ErrorGroupItem = APIReturnType<'GET /api/apm/services/{serviceName}/errors'>['errorGroups'][0]; interface Props { - items: ErrorGroupListAPIResponse; + items: ErrorGroupItem[]; serviceName: string; } @@ -128,7 +128,7 @@ function ErrorGroupList({ items, serviceName }: Props) { field: 'message', sortable: false, width: '50%', - render: (message: string, item: ErrorGroupListAPIResponse[0]) => { + render: (message: string, item: ErrorGroupItem) => { return ( diff --git a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx index f4870439fe478..fc218f3ba6df3 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx @@ -39,7 +39,7 @@ function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) { urlParams: { kuery, start, end }, } = useUrlParams(); - const { data: items = [] } = useFetcher( + const { data } = useFetcher( (callApmApi) => { if (!start || !end) { return undefined; @@ -61,6 +61,7 @@ function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) { [kuery, serviceName, start, end] ); + const items = data?.serviceNodes ?? []; const columns: Array> = [ { name: ( diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx index a4647bc148b1e..4ff42b151dc8e 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx @@ -164,7 +164,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { }, ]; - const { data = [], status } = useFetcher( + const { data, status } = useFetcher( (callApmApi) => { if (!start || !end) { return; @@ -188,8 +188,10 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { [start, end, serviceName, environment] ); + const serviceDependencies = data?.serviceDependencies ?? []; + // need top-level sortable fields for the managed table - const items = data.map((item) => ({ + const items = serviceDependencies.map((item) => ({ ...item, errorRateValue: item.errorRate.value, latencyValue: item.latency.value, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx index c10ec1052f2a2..13322b094c65e 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx @@ -12,6 +12,7 @@ import uuid from 'uuid'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { getTimeRangeComparison } from '../../shared/time_comparison/get_time_range_comparison'; import { ServiceOverviewInstancesTable, @@ -30,20 +31,24 @@ interface ServiceOverviewInstancesChartAndTableProps { serviceName: string; } -const INITIAL_STATE = { - items: [] as Array<{ - serviceNodeName: string; - errorRate: number; - throughput: number; - latency: number; - cpuUsage: number; - memoryUsage: number; - }>, - requestId: undefined, - totalItems: 0, +export interface PrimaryStatsServiceInstanceItem { + serviceNodeName: string; + errorRate: number; + throughput: number; + latency: number; + cpuUsage: number; + memoryUsage: number; +} + +const INITIAL_STATE_PRIMARY_STATS = { + primaryStatsItems: [] as PrimaryStatsServiceInstanceItem[], + primaryStatsRequestId: undefined, + primaryStatsItemCount: 0, }; -const INITIAL_STATE_COMPARISON_STATISTICS = { +type ApiResponseComparisonStats = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/comparison_statistics'>; + +const INITIAL_STATE_COMPARISON_STATISTICS: ApiResponseComparisonStats = { currentPeriod: {}, previousPeriod: {}, }; @@ -93,7 +98,10 @@ export function ServiceOverviewInstancesChartAndTable({ comparisonType, }); - const { data = INITIAL_STATE, status } = useFetcher( + const { + data: primaryStatsData = INITIAL_STATE_PRIMARY_STATS, + status: primaryStatsStatus, + } = useFetcher( (callApmApi) => { if (!start || !end || !transactionType || !latencyAggregationType) { return; @@ -116,9 +124,9 @@ export function ServiceOverviewInstancesChartAndTable({ }, }, }).then((response) => { - const tableItems = orderBy( + const primaryStatsItems = orderBy( // need top-level sortable fields for the managed table - response.map((item) => ({ + response.serviceInstances.map((item) => ({ ...item, latency: item.latency ?? 0, throughput: item.throughput ?? 0, @@ -131,9 +139,9 @@ export function ServiceOverviewInstancesChartAndTable({ ).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE); return { - requestId: uuid(), - items: tableItems, - totalItems: response.length, + primaryStatsRequestId: uuid(), + primaryStatsItems, + primaryStatsItemCount: response.serviceInstances.length, }; }); }, @@ -154,10 +162,14 @@ export function ServiceOverviewInstancesChartAndTable({ ] ); - const { items, requestId, totalItems } = data; + const { + primaryStatsItems, + primaryStatsRequestId, + primaryStatsItemCount, + } = primaryStatsData; const { - data: comparisonStatistics = INITIAL_STATE_COMPARISON_STATISTICS, + data: comparisonStatsData = INITIAL_STATE_COMPARISON_STATISTICS, status: comparisonStatisticsStatus, } = useFetcher( (callApmApi) => { @@ -166,7 +178,7 @@ export function ServiceOverviewInstancesChartAndTable({ !end || !transactionType || !latencyAggregationType || - !totalItems + !primaryStatsItemCount ) { return; } @@ -187,7 +199,7 @@ export function ServiceOverviewInstancesChartAndTable({ numBuckets: 20, transactionType, serviceNodeIds: JSON.stringify( - items.map((item) => item.serviceNodeName) + primaryStatsItems.map((item) => item.serviceNodeName) ), comparisonStart, comparisonEnd, @@ -197,7 +209,7 @@ export function ServiceOverviewInstancesChartAndTable({ }, // only fetches comparison statistics when requestId is invalidated by primary statistics api call // eslint-disable-next-line react-hooks/exhaustive-deps - [requestId], + [primaryStatsRequestId], { preservePreviousData: false } ); @@ -213,14 +225,14 @@ export function ServiceOverviewInstancesChartAndTable({ { diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx index f2a169eb31f98..b88172a162063 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx @@ -8,7 +8,6 @@ import { EuiBasicTableColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { ValuesType } from 'utility-types'; import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; import { isJavaAgentName } from '../../../../../common/agent_name'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n'; @@ -25,10 +24,7 @@ import { MetricOverviewLink } from '../../../shared/Links/apm/MetricOverviewLink import { ServiceNodeMetricOverviewLink } from '../../../shared/Links/apm/ServiceNodeMetricOverviewLink'; import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; import { getLatencyColumnLabel } from '../get_latency_column_label'; - -type ServiceInstancePrimaryStatisticItem = ValuesType< - APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/primary_statistics'> ->; +import { PrimaryStatsServiceInstanceItem } from '../service_overview_instances_chart_and_table'; type ServiceInstanceComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/comparison_statistics'>; @@ -36,15 +32,15 @@ export function getColumns({ serviceName, agentName, latencyAggregationType, - serviceInstanceComparisonStatistics, + comparisonStatsData, comparisonEnabled, }: { serviceName: string; agentName?: string; latencyAggregationType?: LatencyAggregationType; - serviceInstanceComparisonStatistics?: ServiceInstanceComparisonStatistics; + comparisonStatsData?: ServiceInstanceComparisonStatistics; comparisonEnabled?: boolean; -}): Array> { +}): Array> { return [ { field: 'serviceNodeName', @@ -91,11 +87,9 @@ export function getColumns({ width: px(unit * 10), render: (_, { serviceNodeName, latency }) => { const currentPeriodTimestamp = - serviceInstanceComparisonStatistics?.currentPeriod?.[serviceNodeName] - ?.latency; + comparisonStatsData?.currentPeriod?.[serviceNodeName]?.latency; const previousPeriodTimestamp = - serviceInstanceComparisonStatistics?.previousPeriod?.[serviceNodeName] - ?.latency; + comparisonStatsData?.previousPeriod?.[serviceNodeName]?.latency; return ( { const currentPeriodTimestamp = - serviceInstanceComparisonStatistics?.currentPeriod?.[serviceNodeName] - ?.throughput; + comparisonStatsData?.currentPeriod?.[serviceNodeName]?.throughput; const previousPeriodTimestamp = - serviceInstanceComparisonStatistics?.previousPeriod?.[serviceNodeName] - ?.throughput; + comparisonStatsData?.previousPeriod?.[serviceNodeName]?.throughput; return ( { const currentPeriodTimestamp = - serviceInstanceComparisonStatistics?.currentPeriod?.[serviceNodeName] - ?.errorRate; + comparisonStatsData?.currentPeriod?.[serviceNodeName]?.errorRate; const previousPeriodTimestamp = - serviceInstanceComparisonStatistics?.previousPeriod?.[serviceNodeName] - ?.errorRate; + comparisonStatsData?.previousPeriod?.[serviceNodeName]?.errorRate; return ( { const currentPeriodTimestamp = - serviceInstanceComparisonStatistics?.currentPeriod?.[serviceNodeName] - ?.cpuUsage; + comparisonStatsData?.currentPeriod?.[serviceNodeName]?.cpuUsage; const previousPeriodTimestamp = - serviceInstanceComparisonStatistics?.previousPeriod?.[serviceNodeName] - ?.cpuUsage; + comparisonStatsData?.previousPeriod?.[serviceNodeName]?.cpuUsage; return ( { const currentPeriodTimestamp = - serviceInstanceComparisonStatistics?.currentPeriod?.[serviceNodeName] - ?.memoryUsage; + comparisonStatsData?.currentPeriod?.[serviceNodeName]?.memoryUsage; const previousPeriodTimestamp = - serviceInstanceComparisonStatistics?.previousPeriod?.[serviceNodeName] - ?.memoryUsage; + comparisonStatsData?.previousPeriod?.[serviceNodeName]?.memoryUsage; return ( ->; - type ServiceInstanceComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/comparison_statistics'>; export interface TableOptions { @@ -42,26 +38,26 @@ export interface TableOptions { } interface Props { - items?: ServiceInstanceItem[]; + primaryStatsItems: PrimaryStatsServiceInstanceItem[]; serviceName: string; - status: FETCH_STATUS; - totalItems: number; + primaryStatsStatus: FETCH_STATUS; + primaryStatsItemCount: number; tableOptions: TableOptions; onChangeTableOptions: (newTableOptions: { page?: { index: number }; sort?: { field: string; direction: SortDirection }; }) => void; - serviceInstanceComparisonStatistics?: ServiceInstanceComparisonStatistics; + comparisonStatsData?: ServiceInstanceComparisonStatistics; isLoading: boolean; } export function ServiceOverviewInstancesTable({ - items = [], - totalItems, + primaryStatsItems = [], + primaryStatsItemCount, serviceName, - status, + primaryStatsStatus: status, tableOptions, onChangeTableOptions, - serviceInstanceComparisonStatistics, + comparisonStatsData: comparisonStatsData, isLoading, }: Props) { const { agentName } = useApmServiceContext(); @@ -76,14 +72,14 @@ export function ServiceOverviewInstancesTable({ agentName, serviceName, latencyAggregationType, - serviceInstanceComparisonStatistics, + comparisonStatsData, comparisonEnabled, }); const pagination = { pageIndex, pageSize: PAGE_SIZE, - totalItemCount: totalItems, + totalItemCount: primaryStatsItemCount, hidePerPageOptions: true, }; @@ -101,11 +97,11 @@ export function ServiceOverviewInstancesTable({ ; const INITIAL_STATE = { - transactionGroups: [], + transactionGroups: [] as ApiResponse['transactionGroups'], isAggregationAccurate: true, requestId: '', transactionGroupsTotalItems: 0, diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx index 23adbb23b2322..94391b5b2fb06 100644 --- a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx @@ -19,6 +19,7 @@ import { } from '../../../../common/profiling'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useFetcher } from '../../../hooks/use_fetcher'; +import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { SearchBar } from '../../shared/search_bar'; import { ServiceProfilingFlamegraph } from './service_profiling_flamegraph'; import { ServiceProfilingTimeline } from './service_profiling_timeline'; @@ -28,6 +29,9 @@ interface ServiceProfilingProps { environment?: string; } +type ApiResponse = APIReturnType<'GET /api/apm/services/{serviceName}/profiling/timeline'>; +const DEFAULT_DATA: ApiResponse = { profilingTimeline: [] }; + export function ServiceProfiling({ serviceName, environment, @@ -36,7 +40,7 @@ export function ServiceProfiling({ urlParams: { kuery, start, end }, } = useUrlParams(); - const { data = [] } = useFetcher( + const { data = DEFAULT_DATA } = useFetcher( (callApmApi) => { if (!start || !end) { return; @@ -58,14 +62,16 @@ export function ServiceProfiling({ [kuery, start, end, serviceName, environment] ); + const { profilingTimeline } = data; + const [valueType, setValueType] = useState(); useEffect(() => { - if (!data.length) { + if (!profilingTimeline.length) { return; } - const availableValueTypes = data.reduce((set, point) => { + const availableValueTypes = profilingTimeline.reduce((set, point) => { (Object.keys(point.valueTypes).filter( (type) => type !== 'unknown' ) as ProfilingValueType[]) @@ -80,7 +86,7 @@ export function ServiceProfiling({ if (!valueType || !availableValueTypes.has(valueType)) { setValueType(Array.from(availableValueTypes)[0]); } - }, [data, valueType]); + }, [profilingTimeline, valueType]); return ( <> @@ -103,7 +109,7 @@ export function ServiceProfiling({ { setValueType(type); }} diff --git a/x-pack/plugins/apm/public/components/shared/ApmHeader/apm_header.stories.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/apm_header.stories.tsx index 3cd858aceaa90..4bc9764b704b0 100644 --- a/x-pack/plugins/apm/public/components/shared/ApmHeader/apm_header.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/ApmHeader/apm_header.stories.tsx @@ -8,7 +8,7 @@ import { EuiTitle } from '@elastic/eui'; import React, { ComponentType } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { HttpSetup } from '../../../../../../../src/core/public'; +import { CoreStart } from '../../../../../../../src/core/public'; import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; @@ -20,7 +20,7 @@ export default { component: ApmHeader, decorators: [ (Story: ComponentType) => { - createCallApmApi(({} as unknown) as HttpSetup); + createCallApmApi(({} as unknown) as CoreStart); return ( diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx index 6f2910a2a5ef7..a624c220a0e4c 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.test.tsx @@ -9,7 +9,6 @@ import { act, fireEvent, render } from '@testing-library/react'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { CustomLinkMenuSection } from '.'; -import { CustomLink as CustomLinkType } from '../../../../../common/custom_link/custom_link_types'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import * as useFetcher from '../../../../hooks/use_fetcher'; @@ -40,7 +39,7 @@ const transaction = ({ describe('Custom links', () => { it('shows empty message when no custom link is available', () => { jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ - data: [], + data: { customLinks: [] }, status: useFetcher.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); @@ -58,7 +57,7 @@ describe('Custom links', () => { it('shows loading while custom links are fetched', () => { jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ - data: [], + data: { customLinks: [] }, status: useFetcher.FETCH_STATUS.LOADING, refetch: jest.fn(), }); @@ -71,12 +70,14 @@ describe('Custom links', () => { }); it('shows first 3 custom links available', () => { - const customLinks = [ - { id: '1', label: 'foo', url: 'foo' }, - { id: '2', label: 'bar', url: 'bar' }, - { id: '3', label: 'baz', url: 'baz' }, - { id: '4', label: 'qux', url: 'qux' }, - ] as CustomLinkType[]; + const customLinks = { + customLinks: [ + { id: '1', label: 'foo', url: 'foo' }, + { id: '2', label: 'bar', url: 'bar' }, + { id: '3', label: 'baz', url: 'baz' }, + { id: '4', label: 'qux', url: 'qux' }, + ], + }; jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ data: customLinks, @@ -93,15 +94,17 @@ describe('Custom links', () => { }); it('clicks "show all" and "show fewer"', () => { - const customLinks = [ - { id: '1', label: 'foo', url: 'foo' }, - { id: '2', label: 'bar', url: 'bar' }, - { id: '3', label: 'baz', url: 'baz' }, - { id: '4', label: 'qux', url: 'qux' }, - ] as CustomLinkType[]; + const data = { + customLinks: [ + { id: '1', label: 'foo', url: 'foo' }, + { id: '2', label: 'bar', url: 'bar' }, + { id: '3', label: 'baz', url: 'baz' }, + { id: '4', label: 'qux', url: 'qux' }, + ], + }; jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ - data: customLinks, + data, status: useFetcher.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); @@ -125,7 +128,7 @@ describe('Custom links', () => { describe('create custom link buttons', () => { it('shows create button below empty message', () => { jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ - data: [], + data: { customLinks: [] }, status: useFetcher.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); @@ -140,15 +143,17 @@ describe('Custom links', () => { }); it('shows create button besides the title', () => { - const customLinks = [ - { id: '1', label: 'foo', url: 'foo' }, - { id: '2', label: 'bar', url: 'bar' }, - { id: '3', label: 'baz', url: 'baz' }, - { id: '4', label: 'qux', url: 'qux' }, - ] as CustomLinkType[]; + const data = { + customLinks: [ + { id: '1', label: 'foo', url: 'foo' }, + { id: '2', label: 'bar', url: 'bar' }, + { id: '3', label: 'baz', url: 'baz' }, + { id: '4', label: 'qux', url: 'qux' }, + ], + }; jest.spyOn(useFetcher, 'useFetcher').mockReturnValue({ - data: customLinks, + data, status: useFetcher.FETCH_STATUS.SUCCESS, refetch: jest.fn(), }); diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx index 7d2e4a13278ec..cbbf34c78c4af 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLinkMenuSection/index.tsx @@ -58,7 +58,7 @@ export function CustomLinkMenuSection({ [transaction] ); - const { data: customLinks = [], status, refetch } = useFetcher( + const { data, status, refetch } = useFetcher( (callApmApi) => callApmApi({ isCachable: false, @@ -68,6 +68,8 @@ export function CustomLinkMenuSection({ [filters] ); + const customLinks = data?.customLinks ?? []; + return ( <> {isCreateEditFlyoutOpen && ( diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/helper.test.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.test.ts index b0ac35cc3667a..b8d67f71a9baa 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/helper.test.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.test.ts @@ -7,7 +7,7 @@ import { onBrushEnd, isTimeseriesEmpty } from './helper'; import { History } from 'history'; -import { TimeSeries } from '../../../../../typings/timeseries'; +import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; describe('Chart helper', () => { describe('onBrushEnd', () => { @@ -52,7 +52,7 @@ describe('Chart helper', () => { type: 'line', color: 'red', }, - ] as TimeSeries[]; + ] as Array>; expect(isTimeseriesEmpty(timeseries)).toBeTruthy(); }); it('returns true when y coordinate is null', () => { @@ -63,7 +63,7 @@ describe('Chart helper', () => { type: 'line', color: 'red', }, - ] as TimeSeries[]; + ] as Array>; expect(isTimeseriesEmpty(timeseries)).toBeTruthy(); }); it('returns true when y coordinate is undefined', () => { @@ -74,7 +74,7 @@ describe('Chart helper', () => { type: 'line', color: 'red', }, - ] as TimeSeries[]; + ] as Array>; expect(isTimeseriesEmpty(timeseries)).toBeTruthy(); }); it('returns false when at least one coordinate is filled', () => { @@ -91,7 +91,7 @@ describe('Chart helper', () => { type: 'line', color: 'green', }, - ] as TimeSeries[]; + ] as Array>; expect(isTimeseriesEmpty(timeseries)).toBeFalsy(); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts index 3b93cb1f402e8..d94f2ce8f5c5d 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts @@ -7,7 +7,7 @@ import { XYBrushArea } from '@elastic/charts'; import { History } from 'history'; -import { TimeSeries } from '../../../../../typings/timeseries'; +import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; import { fromQuery, toQuery } from '../../Links/url_helpers'; export const onBrushEnd = ({ @@ -36,15 +36,12 @@ export const onBrushEnd = ({ } }; -export function isTimeseriesEmpty(timeseries?: TimeSeries[]) { +export function isTimeseriesEmpty(timeseries?: Array>) { return ( !timeseries || timeseries .map((serie) => serie.data) .flat() - .every( - ({ y }: { x?: number | null; y?: number | null }) => - y === null || y === undefined - ) + .every(({ y }: Coordinate) => y === null || y === undefined) ); } diff --git a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx index 5f24c1ee2495b..5bcf0d161653e 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx @@ -23,13 +23,13 @@ import { } from '../../../../../common/utils/formatters'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useTheme } from '../../../../hooks/use_theme'; -import { APIReturnType } from '../../../../services/rest/createCallApmApi'; +import { PrimaryStatsServiceInstanceItem } from '../../../app/service_overview/service_overview_instances_chart_and_table'; import { ChartContainer } from '../chart_container'; import { getResponseTimeTickFormatter } from '../transaction_charts/helper'; interface InstancesLatencyDistributionChartProps { height: number; - items?: APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/primary_statistics'>; + items?: PrimaryStatsServiceInstanceItem[]; status: FETCH_STATUS; } diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx index 8009f288d48c0..2c9601d709cb4 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx @@ -28,7 +28,11 @@ import React from 'react'; import { useHistory } from 'react-router-dom'; import { useChartTheme } from '../../../../../observability/public'; import { asAbsoluteDateTime } from '../../../../common/utils/formatters'; -import { RectCoordinate, TimeSeries } from '../../../../typings/timeseries'; +import { + Coordinate, + RectCoordinate, + TimeSeries, +} from '../../../../typings/timeseries'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { useTheme } from '../../../hooks/use_theme'; import { useAnnotationsContext } from '../../../context/annotations/use_annotations_context'; @@ -43,7 +47,7 @@ interface Props { fetchStatus: FETCH_STATUS; height?: number; onToggleLegend?: LegendItemListener; - timeseries: TimeSeries[]; + timeseries: Array>; /** * Formatter for y-axis tick values */ @@ -85,12 +89,10 @@ export function TimeseriesChart({ const max = Math.max(...xValues); const xFormatter = niceTimeFormatter([min, max]); - const isEmpty = isTimeseriesEmpty(timeseries); - const annotationColor = theme.eui.euiColorSecondary; - const allSeries = [...timeseries, ...(anomalyTimeseries?.boundaries ?? [])]; + const xDomain = isEmpty ? { min: 0, max: 1 } : { min, max }; return ( @@ -111,7 +113,7 @@ export function TimeseriesChart({ showLegend showLegendExtra legendPosition={Position.Bottom} - xDomain={{ min, max }} + xDomain={xDomain} onLegendItemClick={(legend) => { if (onToggleLegend) { onToggleLegend(legend); diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx index f55389ec2d5f7..23016cc5dd8e9 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx @@ -28,7 +28,7 @@ import { asAbsoluteDateTime, asPercent, } from '../../../../../common/utils/formatters'; -import { TimeSeries } from '../../../../../typings/timeseries'; +import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useTheme } from '../../../../hooks/use_theme'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; @@ -42,7 +42,7 @@ interface Props { fetchStatus: FETCH_STATUS; height?: number; showAnnotations: boolean; - timeseries?: TimeSeries[]; + timeseries?: Array>; } export function TransactionBreakdownChartContents({ diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.tsx index 6c46580f4738e..31d18b7a9709d 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.tsx @@ -6,14 +6,14 @@ */ import { isFiniteNumber } from '../../../../../common/utils/is_finite_number'; -import { APMChartSpec, Coordinate } from '../../../../../typings/timeseries'; +import { Coordinate } from '../../../../../typings/timeseries'; import { TimeFormatter } from '../../../../../common/utils/formatters'; export function getResponseTimeTickFormatter(formatter: TimeFormatter) { return (t: number) => formatter(t).formatted; } -export function getMaxY(specs?: Array>) { +export function getMaxY(specs?: Array<{ data: Coordinate[] }>) { const values = specs ?.flatMap((spec) => spec.data) .map((coord) => coord.y) diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.tsx index 2bd3fef8c0e88..1018b9eca2119 100644 --- a/x-pack/plugins/apm/public/components/shared/search_bar.tsx +++ b/x-pack/plugins/apm/public/components/shared/search_bar.tsx @@ -7,14 +7,21 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCallOut } from '@elastic/eui'; +import { EuiLink } from '@elastic/eui'; +import { enableInspectEsQueries } from '../../../../observability/public'; import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; import { px, unit } from '../../style/variables'; import { DatePicker } from './DatePicker'; import { KueryBar } from './KueryBar'; import { TimeComparison } from './time_comparison'; import { useBreakPoints } from '../../hooks/use_break_points'; +import { useKibanaUrl } from '../../hooks/useKibanaUrl'; +import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; -const SearchBarFlexGroup = euiStyled(EuiFlexGroup)` +const EuiFlexGroupSpaced = euiStyled(EuiFlexGroup)` margin: ${({ theme }) => `${theme.eui.euiSizeS} ${theme.eui.euiSizeS} -${theme.eui.gutterTypes.gutterMedium} ${theme.eui.euiSizeS}`}; `; @@ -29,6 +36,52 @@ function getRowDirection(showColumn: boolean) { return showColumn ? 'column' : 'row'; } +function DebugQueryCallout() { + const { uiSettings } = useApmPluginContext().core; + const advancedSettingsUrl = useKibanaUrl('/app/management/kibana/settings', { + query: { + query: 'category:(observability)', + }, + }); + + if (!uiSettings.get(enableInspectEsQueries)) { + return null; + } + + return ( + + + + + {i18n.translate( + 'xpack.apm.searchBar.inspectEsQueriesEnabled.callout.description.advancedSettings', + { defaultMessage: 'Advanced Setting' } + )} + + ), + }} + /> + + + + ); +} + export function SearchBar({ prepend, showTimeComparison = false, @@ -38,26 +91,29 @@ export function SearchBar({ const itemsStyle = { marginBottom: isLarge ? px(unit) : 0 }; return ( - - - - - - - {showTimeComparison && ( - - + <> + + + + + + + + {showTimeComparison && ( + + + + )} + + - )} - - - - - - + + + + ); } diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index 024deca558497..9a910787d5fe8 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -116,8 +116,8 @@ export function MockApmPluginContextWrapper({ children?: React.ReactNode; value?: ApmPluginContextValue; }) { - if (value.core?.http) { - createCallApmApi(value.core?.http); + if (value.core) { + createCallApmApi(value.core); } return ( { if (start && end) { return callApmApi({ @@ -51,9 +53,9 @@ export function useEnvironmentsFetcher({ ); const environmentOptions = useMemo( - () => getEnvironmentOptions(environments), - [environments] + () => getEnvironmentOptions(data.environments), + [data?.environments] ); - return { environments, status, environmentOptions }; + return { environments: data.environments, status, environmentOptions }; } diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 5740e47d0076f..382053f133950 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -85,19 +85,19 @@ export class ApmPlugin implements Plugin { const getApmDataHelper = async () => { const { fetchObservabilityOverviewPageData, - hasData, + getHasData, createCallApmApi, } = await import('./services/rest/apm_observability_overview_fetchers'); // have to do this here as well in case app isn't mounted yet - createCallApmApi(core.http); + createCallApmApi(core); - return { fetchObservabilityOverviewPageData, hasData }; + return { fetchObservabilityOverviewPageData, getHasData }; }; plugins.observability.dashboard.register({ appName: 'apm', hasData: async () => { const dataHelper = await getApmDataHelper(); - return await dataHelper.hasData(); + return await dataHelper.getHasData(); }, fetchData: async (params: FetchDataParams) => { const dataHelper = await getApmDataHelper(); @@ -112,7 +112,7 @@ export class ApmPlugin implements Plugin { createCallApmApi, } = await import('./components/app/RumDashboard/ux_overview_fetchers'); // have to do this here as well in case app isn't mounted yet - createCallApmApi(core.http); + createCallApmApi(core); return { fetchUxOverviewDate, hasRumData }; }; diff --git a/x-pack/plugins/apm/public/selectors/throughput_chart_selectors.ts b/x-pack/plugins/apm/public/selectors/throughput_chart_selectors.ts index f9e72bff231f4..f334212536778 100644 --- a/x-pack/plugins/apm/public/selectors/throughput_chart_selectors.ts +++ b/x-pack/plugins/apm/public/selectors/throughput_chart_selectors.ts @@ -8,14 +8,14 @@ import { difference, zipObject } from 'lodash'; import { EuiTheme } from '../../../../../src/plugins/kibana_react/common'; import { asTransactionRate } from '../../common/utils/formatters'; -import { TimeSeries } from '../../typings/timeseries'; +import { Coordinate, TimeSeries } from '../../typings/timeseries'; import { APIReturnType } from '../services/rest/createCallApmApi'; import { httpStatusCodeToColor } from '../utils/httpStatusCodeToColor'; export type ThroughputChartsResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/throughput'>; export interface ThroughputChart { - throughputTimeseries: TimeSeries[]; + throughputTimeseries: Array>; } export function getThroughputChartSelector({ diff --git a/x-pack/plugins/apm/public/services/callApi.test.ts b/x-pack/plugins/apm/public/services/callApi.test.ts index cdd9cb5b08a32..5f0be1b6fadbb 100644 --- a/x-pack/plugins/apm/public/services/callApi.test.ts +++ b/x-pack/plugins/apm/public/services/callApi.test.ts @@ -7,49 +7,51 @@ import { mockNow } from '../utils/testHelpers'; import { clearCache, callApi } from './rest/callApi'; -import { SessionStorageMock } from './__mocks__/SessionStorageMock'; -import { HttpSetup } from 'kibana/public'; +import { CoreStart, HttpSetup } from 'kibana/public'; -type HttpMock = HttpSetup & { - get: jest.SpyInstance; +type CoreMock = CoreStart & { + http: { + get: jest.SpyInstance; + }; }; describe('callApi', () => { - let http: HttpMock; + let core: CoreMock; beforeEach(() => { - http = ({ - get: jest.fn().mockReturnValue({ - my_key: 'hello_world', - }), - } as unknown) as HttpMock; - - // @ts-expect-error - global.sessionStorage = new SessionStorageMock(); + core = ({ + http: { + get: jest.fn().mockReturnValue({ + my_key: 'hello_world', + }), + }, + uiSettings: { get: () => false }, // disable `observability:enableInspectEsQueries` setting + } as unknown) as CoreMock; }); afterEach(() => { - http.get.mockClear(); + core.http.get.mockClear(); clearCache(); }); - describe('apm_debug', () => { + describe('_inspect', () => { beforeEach(() => { - sessionStorage.setItem('apm_debug', 'true'); + // @ts-expect-error + core.uiSettings.get = () => true; // enable `observability:enableInspectEsQueries` setting }); it('should add debug param for APM endpoints', async () => { - await callApi(http, { pathname: `/api/apm/status/server` }); + await callApi(core, { pathname: `/api/apm/status/server` }); - expect(http.get).toHaveBeenCalledWith('/api/apm/status/server', { - query: { _debug: true }, + expect(core.http.get).toHaveBeenCalledWith('/api/apm/status/server', { + query: { _inspect: true }, }); }); it('should not add debug param for non-APM endpoints', async () => { - await callApi(http, { pathname: `/api/kibana` }); + await callApi(core, { pathname: `/api/kibana` }); - expect(http.get).toHaveBeenCalledWith('/api/kibana', { query: {} }); + expect(core.http.get).toHaveBeenCalledWith('/api/kibana', { query: {} }); }); }); @@ -65,138 +67,138 @@ describe('callApi', () => { describe('when the call does not contain start/end params', () => { it('should not return cached response for identical calls', async () => { - await callApi(http, { pathname: `/api/kibana`, query: { foo: 'bar' } }); - await callApi(http, { pathname: `/api/kibana`, query: { foo: 'bar' } }); - await callApi(http, { pathname: `/api/kibana`, query: { foo: 'bar' } }); + await callApi(core, { pathname: `/api/kibana`, query: { foo: 'bar' } }); + await callApi(core, { pathname: `/api/kibana`, query: { foo: 'bar' } }); + await callApi(core, { pathname: `/api/kibana`, query: { foo: 'bar' } }); - expect(http.get).toHaveBeenCalledTimes(3); + expect(core.http.get).toHaveBeenCalledTimes(3); }); }); describe('when the call contains start/end params', () => { it('should return cached response for identical calls', async () => { - await callApi(http, { + await callApi(core, { pathname: `/api/kibana`, query: { start: '2010', end: '2011' }, }); - await callApi(http, { + await callApi(core, { pathname: `/api/kibana`, query: { start: '2010', end: '2011' }, }); - await callApi(http, { + await callApi(core, { pathname: `/api/kibana`, query: { start: '2010', end: '2011' }, }); - expect(http.get).toHaveBeenCalledTimes(1); + expect(core.http.get).toHaveBeenCalledTimes(1); }); it('should not return cached response for subsequent calls if arguments change', async () => { - await callApi(http, { + await callApi(core, { pathname: `/api/kibana`, query: { start: '2010', end: '2011', foo: 'bar1' }, }); - await callApi(http, { + await callApi(core, { pathname: `/api/kibana`, query: { start: '2010', end: '2011', foo: 'bar2' }, }); - await callApi(http, { + await callApi(core, { pathname: `/api/kibana`, query: { start: '2010', end: '2011', foo: 'bar3' }, }); - expect(http.get).toHaveBeenCalledTimes(3); + expect(core.http.get).toHaveBeenCalledTimes(3); }); it('should not return cached response if `end` is a future timestamp', async () => { - await callApi(http, { + await callApi(core, { pathname: `/api/kibana`, query: { end: '2030' }, }); - await callApi(http, { + await callApi(core, { pathname: `/api/kibana`, query: { end: '2030' }, }); - await callApi(http, { + await callApi(core, { pathname: `/api/kibana`, query: { end: '2030' }, }); - expect(http.get).toHaveBeenCalledTimes(3); + expect(core.http.get).toHaveBeenCalledTimes(3); }); it('should return cached response if calls contain `end` param in the past', async () => { - await callApi(http, { + await callApi(core, { pathname: `/api/kibana`, query: { start: '2009', end: '2010' }, }); - await callApi(http, { + await callApi(core, { pathname: `/api/kibana`, query: { start: '2009', end: '2010' }, }); - await callApi(http, { + await callApi(core, { pathname: `/api/kibana`, query: { start: '2009', end: '2010' }, }); - expect(http.get).toHaveBeenCalledTimes(1); + expect(core.http.get).toHaveBeenCalledTimes(1); }); it('should return cached response even if order of properties change', async () => { - await callApi(http, { + await callApi(core, { pathname: `/api/kibana`, query: { end: '2010', start: '2009' }, }); - await callApi(http, { + await callApi(core, { pathname: `/api/kibana`, query: { start: '2009', end: '2010' }, }); - await callApi(http, { + await callApi(core, { query: { start: '2009', end: '2010' }, pathname: `/api/kibana`, }); - expect(http.get).toHaveBeenCalledTimes(1); + expect(core.http.get).toHaveBeenCalledTimes(1); }); it('should not return cached response with `isCachable: false` option', async () => { - await callApi(http, { + await callApi(core, { isCachable: false, pathname: `/api/kibana`, query: { start: '2010', end: '2011' }, }); - await callApi(http, { + await callApi(core, { isCachable: false, pathname: `/api/kibana`, query: { start: '2010', end: '2011' }, }); - await callApi(http, { + await callApi(core, { isCachable: false, pathname: `/api/kibana`, query: { start: '2010', end: '2011' }, }); - expect(http.get).toHaveBeenCalledTimes(3); + expect(core.http.get).toHaveBeenCalledTimes(3); }); it('should return cached response with `isCachable: true` option', async () => { - await callApi(http, { + await callApi(core, { isCachable: true, pathname: `/api/kibana`, query: { end: '2030' }, }); - await callApi(http, { + await callApi(core, { isCachable: true, pathname: `/api/kibana`, query: { end: '2030' }, }); - await callApi(http, { + await callApi(core, { isCachable: true, pathname: `/api/kibana`, query: { end: '2030' }, }); - expect(http.get).toHaveBeenCalledTimes(1); + expect(core.http.get).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/apm/public/services/callApmApi.test.ts b/x-pack/plugins/apm/public/services/callApmApi.test.ts index 25d34b5d102f5..56146c49fc57d 100644 --- a/x-pack/plugins/apm/public/services/callApmApi.test.ts +++ b/x-pack/plugins/apm/public/services/callApmApi.test.ts @@ -7,7 +7,7 @@ import * as callApiExports from './rest/callApi'; import { createCallApmApi, callApmApi } from './rest/createCallApmApi'; -import { HttpSetup } from 'kibana/public'; +import { CoreStart } from 'kibana/public'; const callApi = jest .spyOn(callApiExports, 'callApi') @@ -15,7 +15,7 @@ const callApi = jest describe('callApmApi', () => { beforeEach(() => { - createCallApmApi({} as HttpSetup); + createCallApmApi({} as CoreStart); }); afterEach(() => { @@ -79,7 +79,7 @@ describe('callApmApi', () => { {}, expect.objectContaining({ pathname: '/api/apm', - method: 'POST', + method: 'post', body: { foo: 'bar', bar: 'foo', diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts index b0bae6aa91a3d..1821e92ee5a78 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts @@ -8,7 +8,7 @@ import moment from 'moment'; import { fetchObservabilityOverviewPageData, - hasData, + getHasData, } from './apm_observability_overview_fetchers'; import * as createCallApmApi from './createCallApmApi'; @@ -31,12 +31,12 @@ describe('Observability dashboard data', () => { describe('hasData', () => { it('returns false when no data is available', async () => { callApmApiMock.mockImplementation(() => Promise.resolve(false)); - const response = await hasData(); + const response = await getHasData(); expect(response).toBeFalsy(); }); it('returns true when data is available', async () => { - callApmApiMock.mockImplementation(() => Promise.resolve(true)); - const response = await hasData(); + callApmApiMock.mockResolvedValue({ hasData: true }); + const response = await getHasData(); expect(response).toBeTruthy(); }); }); diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts index 6d630ede1cb11..55ead8d942aca 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts @@ -58,9 +58,11 @@ export const fetchObservabilityOverviewPageData = async ({ }; }; -export async function hasData() { - return await callApmApi({ +export async function getHasData() { + const res = await callApmApi({ endpoint: 'GET /api/apm/observability_overview/has_data', signal: null, }); + + return res.hasData; } diff --git a/x-pack/plugins/apm/public/services/rest/callApi.ts b/x-pack/plugins/apm/public/services/rest/callApi.ts index f5106fce78cc7..f623872303c5b 100644 --- a/x-pack/plugins/apm/public/services/rest/callApi.ts +++ b/x-pack/plugins/apm/public/services/rest/callApi.ts @@ -5,15 +5,19 @@ * 2.0. */ -import { HttpSetup } from 'kibana/public'; +import { CoreSetup, CoreStart } from 'kibana/public'; import { isString, startsWith } from 'lodash'; import LRU from 'lru-cache'; import hash from 'object-hash'; +import { enableInspectEsQueries } from '../../../../observability/public'; import { FetchOptions } from '../../../common/fetch_options'; -function fetchOptionsWithDebug(fetchOptions: FetchOptions) { +function fetchOptionsWithDebug( + fetchOptions: FetchOptions, + inspectableEsQueriesEnabled: boolean +) { const debugEnabled = - sessionStorage.getItem('apm_debug') === 'true' && + inspectableEsQueriesEnabled && startsWith(fetchOptions.pathname, '/api/apm'); const { body, ...rest } = fetchOptions; @@ -23,7 +27,7 @@ function fetchOptionsWithDebug(fetchOptions: FetchOptions) { ...(body !== undefined ? { body: JSON.stringify(body) } : {}), query: { ...fetchOptions.query, - ...(debugEnabled ? { _debug: true } : {}), + ...(debugEnabled ? { _inspect: true } : {}), }, }; } @@ -37,9 +41,12 @@ export function clearCache() { export type CallApi = typeof callApi; export async function callApi( - http: HttpSetup, + { http, uiSettings }: CoreStart | CoreSetup, fetchOptions: FetchOptions ): Promise { + const inspectableEsQueriesEnabled: boolean = uiSettings.get( + enableInspectEsQueries + ); const cacheKey = getCacheKey(fetchOptions); const cacheResponse = cache.get(cacheKey); if (cacheResponse) { @@ -47,7 +54,8 @@ export async function callApi( } const { pathname, method = 'get', ...options } = fetchOptionsWithDebug( - fetchOptions + fetchOptions, + inspectableEsQueriesEnabled ); const lowercaseMethod = method.toLowerCase() as diff --git a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts index c6d55a85dd70e..b0cce3296fe21 100644 --- a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts +++ b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts @@ -5,13 +5,14 @@ * 2.0. */ -import { HttpSetup } from 'kibana/public'; +import { CoreSetup, CoreStart } from 'kibana/public'; +import { parseEndpoint } from '../../../common/apm_api/parse_endpoint'; import { FetchOptions } from '../../../common/fetch_options'; import { callApi } from './callApi'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { APMAPI } from '../../../server/routes/create_apm_api'; +import type { APMAPI } from '../../../server/routes/create_apm_api'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Client } from '../../../server/routes/typings'; +import type { Client } from '../../../server/routes/typings'; export type APMClient = Client; export type AutoAbortedAPMClient = Client; @@ -24,8 +25,8 @@ export type APMClientOptions = Omit< signal: AbortSignal | null; params?: { body?: any; - query?: any; - path?: any; + query?: Record; + path?: Record; }; }; @@ -35,23 +36,17 @@ export let callApmApi: APMClient = () => { ); }; -export function createCallApmApi(http: HttpSetup) { +export function createCallApmApi(core: CoreStart | CoreSetup) { callApmApi = ((options: APMClientOptions) => { - const { endpoint, params = {}, ...opts } = options; + const { endpoint, params, ...opts } = options; + const { method, pathname } = parseEndpoint(endpoint, params?.path); - const path = (params.path || {}) as Record; - const [method, pathname] = endpoint.split(' '); - - const formattedPathname = Object.keys(path).reduce((acc, paramName) => { - return acc.replace(`{${paramName}}`, path[paramName]); - }, pathname); - - return callApi(http, { + return callApi(core, { ...opts, method, - pathname: formattedPathname, - body: params.body, - query: params.query, + pathname, + body: params?.body, + query: params?.query, }); }) as APMClient; } diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md index b125407a160aa..ef2675f4f6c65 100644 --- a/x-pack/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -160,10 +160,10 @@ The users will be created with the password specified in kibana.dev.yml for `ela ## Debugging Elasticsearch queries -All APM api endpoints accept `_debug=true` as a query param that will result in the underlying ES query being outputted in the Kibana backend process. +All APM api endpoints accept `_inspect=true` as a query param that will result in the underlying ES query being outputted in the Kibana backend process. Example: -`/api/apm/services/my_service?_debug=true` +`/api/apm/services/my_service?_inspect=true` ## Storybook diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts index 50613c10ff7c0..88b1cf3a344ed 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts @@ -106,7 +106,7 @@ export async function getLatencyDistribution({ type Agg = NonNullable; if (!response.aggregations) { - return; + return {}; } function formatDistribution(distribution: Agg['distribution']) { diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts index aa41880fba444..1f0aa401bcab0 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts @@ -7,8 +7,10 @@ /* eslint-disable no-console */ +import { omit } from 'lodash'; import chalk from 'chalk'; import { KibanaRequest } from '../../../../../../../src/core/server'; +import { inspectableEsQueriesMap } from '../../../routes/create_api'; function formatObj(obj: Record) { return JSON.stringify(obj, null, 2); @@ -18,10 +20,18 @@ export async function callAsyncWithDebug({ cb, getDebugMessage, debug, + request, + requestType, + requestParams, + isCalledWithInternalUser, }: { cb: () => Promise; getDebugMessage: () => { body: string; title: string }; debug: boolean; + request: KibanaRequest; + requestType: string; + requestParams: Record; + isCalledWithInternalUser: boolean; // only allow inspection of queries that were retrieved with credentials of the end user }) { if (!debug) { return cb(); @@ -41,16 +51,27 @@ export async function callAsyncWithDebug({ if (debug) { const highlightColor = esError ? 'bgRed' : 'inverse'; const diff = process.hrtime(startTime); - const duration = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`; + const duration = Math.round(diff[0] * 1000 + diff[1] / 1e6); // duration in ms const { title, body } = getDebugMessage(); console.log( - chalk.bold[highlightColor](`=== Debug: ${title} (${duration}) ===`) + chalk.bold[highlightColor](`=== Debug: ${title} (${duration}ms) ===`) ); console.log(body); console.log(`\n`); + + const inspectableEsQueries = inspectableEsQueriesMap.get(request); + if (!isCalledWithInternalUser && inspectableEsQueries) { + inspectableEsQueries.push({ + response: res, + duration, + requestType, + requestParams: omit(requestParams, 'headers'), + esError: esError?.response ?? esError?.message, + }); + } } if (esError) { @@ -62,13 +83,13 @@ export async function callAsyncWithDebug({ export const getDebugBody = ( params: Record, - operationName: string + requestType: string ) => { - if (operationName === 'search') { + if (requestType === 'search') { return `GET ${params.index}/_search\n${formatObj(params.body)}`; } - return `${chalk.bold('ES operation:')} ${operationName}\n${chalk.bold( + return `${chalk.bold('ES operation:')} ${requestType}\n${chalk.bold( 'ES query:' )}\n${formatObj(params)}`; }; diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index e20103cc6ddca..b8a14253a229a 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -93,6 +93,9 @@ export function createApmEventClient({ ignore_unavailable: true, }; + // only "search" operation is currently supported + const requestType = 'search'; + return callAsyncWithDebug({ cb: () => { const searchPromise = cancelEsRequestOnAbort( @@ -103,10 +106,14 @@ export function createApmEventClient({ return unwrapEsResponse(searchPromise); }, getDebugMessage: () => ({ - body: getDebugBody(searchParams, 'search'), + body: getDebugBody(searchParams, requestType), title: getDebugTitle(request), }), + isCalledWithInternalUser: false, debug, + request, + requestType, + requestParams: searchParams, }); }, }; diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts index 2e83baece01a9..45e17c1678518 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -40,10 +40,10 @@ export function createInternalESClient({ function callEs({ cb, - operationName, + requestType, params, }: { - operationName: string; + requestType: string; cb: () => TransportRequestPromise; params: Record; }) { @@ -51,9 +51,13 @@ export function createInternalESClient({ cb: () => unwrapEsResponse(cancelEsRequestOnAbort(cb(), request)), getDebugMessage: () => ({ title: getDebugTitle(request), - body: getDebugBody(params, operationName), + body: getDebugBody(params, requestType), }), - debug: context.params.query._debug, + debug: context.params.query._inspect, + isCalledWithInternalUser: true, + request, + requestType, + requestParams: params, }); } @@ -65,28 +69,28 @@ export function createInternalESClient({ params: TSearchRequest ): Promise> => { return callEs({ - operationName: 'search', + requestType: 'search', cb: () => asInternalUser.search(params), params, }); }, index: (params: APMIndexDocumentParams) => { return callEs({ - operationName: 'index', + requestType: 'index', cb: () => asInternalUser.index(params), params, }); }, delete: (params: DeleteRequest): Promise<{ result: string }> => { return callEs({ - operationName: 'delete', + requestType: 'delete', cb: () => asInternalUser.delete(params), params, }); }, indicesCreate: (params: CreateIndexRequest) => { return callEs({ - operationName: 'indices.create', + requestType: 'indices.create', cb: () => asInternalUser.indices.create(params), params, }); diff --git a/x-pack/plugins/apm/server/lib/helpers/input_validation.ts b/x-pack/plugins/apm/server/lib/helpers/input_validation.ts index 5c188ff0d093e..0a34711b9b40d 100644 --- a/x-pack/plugins/apm/server/lib/helpers/input_validation.ts +++ b/x-pack/plugins/apm/server/lib/helpers/input_validation.ts @@ -14,7 +14,7 @@ export const withDefaultValidators = ( validators: { [key: string]: Schema } = {} ) => { return Joi.object().keys({ - _debug: Joi.bool(), + _inspect: Joi.bool(), start: dateValidation, end: dateValidation, uiFilters: Joi.string(), diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts index 51f386d59c04a..c0707d0286180 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -51,7 +51,7 @@ function getMockRequest() { ) as APMConfig, params: { query: { - _debug: false, + _inspect: false, }, }, core: { diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 60fb9a8bfa85a..fff661250c6df 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -45,7 +45,7 @@ export interface SetupTimeRange { interface SetupRequestParams { query?: { - _debug?: boolean; + _inspect?: boolean; /** * Timestamp in ms since epoch @@ -88,7 +88,7 @@ export async function setupRequest( indices, apmEventClient: createApmEventClient({ esClient: context.core.elasticsearch.client.asCurrentUser, - debug: context.params.query._debug, + debug: context.params.query._inspect, request, indices, options: { includeFrozen }, diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts index 8d0acb7f85f5d..0b7f82c0b8388 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts @@ -21,20 +21,20 @@ export async function createStaticIndexPattern( setup: Setup, context: APMRequestHandlerContext, savedObjectsClient: InternalSavedObjectsClient -): Promise { +): Promise { return withApmSpan('create_static_index_pattern', async () => { const { config } = context; // don't autocreate APM index pattern if it's been disabled via the config if (!config['xpack.apm.autocreateApmIndexPattern']) { - return; + return false; } // Discover and other apps will throw errors if an index pattern exists without having matching indices. // The following ensures the index pattern is only created if APM data is found const hasData = await hasHistoricalAgentData(setup); if (!hasData) { - return; + return false; } try { @@ -49,12 +49,12 @@ export async function createStaticIndexPattern( { id: APM_STATIC_INDEX_PATTERN_ID, overwrite: false } ) ); - return; + return true; } catch (e) { // if the index pattern (saved object) already exists a conflict error (code: 409) will be thrown // that error should be silenced if (SavedObjectsErrorHelpers.isConflictError(e)) { - return; + return false; } throw e; } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts index abdc8da78502c..bbe13874d7d3b 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/has_data.ts @@ -9,7 +9,7 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { withApmSpan } from '../../utils/with_apm_span'; import { Setup } from '../helpers/setup_request'; -export function hasData({ setup }: { setup: Setup }) { +export function getHasData({ setup }: { setup: Setup }) { return withApmSpan('observability_overview_has_apm_data', async () => { const { apmEventClient } = setup; try { diff --git a/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts b/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts index 32f2238b0ddea..3bebcd49ec34a 100644 --- a/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts +++ b/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts @@ -35,12 +35,14 @@ export const transactionErrorRateChartPreview = createRoute({ options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - const { _debug, ...alertParams } = context.params.query; + const { _inspect, ...alertParams } = context.params.query; - return getTransactionErrorRateChartPreview({ + const errorRateChartPreview = await getTransactionErrorRateChartPreview({ setup, alertParams, }); + + return { errorRateChartPreview }; }, }); @@ -50,11 +52,13 @@ export const transactionErrorCountChartPreview = createRoute({ options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - const { _debug, ...alertParams } = context.params.query; - return getTransactionErrorCountChartPreview({ + const { _inspect, ...alertParams } = context.params.query; + const errorCountChartPreview = await getTransactionErrorCountChartPreview({ setup, alertParams, }); + + return { errorCountChartPreview }; }, }); @@ -64,11 +68,13 @@ export const transactionDurationChartPreview = createRoute({ options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - const { _debug, ...alertParams } = context.params.query; + const { _inspect, ...alertParams } = context.params.query; - return getTransactionDurationChartPreview({ + const latencyChartPreview = await getTransactionDurationChartPreview({ alertParams, setup, }); + + return { latencyChartPreview }; }, }); diff --git a/x-pack/plugins/apm/server/routes/create_api/index.test.ts b/x-pack/plugins/apm/server/routes/create_api/index.test.ts index 01d2797641805..9958b8dec0124 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.test.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.test.ts @@ -48,6 +48,49 @@ const getCoreMock = () => { }; }; +const initApi = (params?: RouteParamsRT) => { + const { mock, context, createRouter, get, post } = getCoreMock(); + const handlerMock = jest.fn(); + createApi() + .add(() => ({ + endpoint: 'GET /foo', + params, + options: { tags: ['access:apm'] }, + handler: handlerMock, + })) + .init(mock, context); + + const routeHandler = get.mock.calls[0][1]; + + const responseMock = { + ok: jest.fn(), + custom: jest.fn(), + }; + + const simulateRequest = (requestMock: any) => { + return routeHandler( + {}, + { + // stub default values + params: {}, + query: {}, + body: null, + ...requestMock, + }, + responseMock + ); + }; + + return { + simulateRequest, + handlerMock, + createRouter, + get, + post, + responseMock, + }; +}; + describe('createApi', () => { it('registers a route with the server', () => { const { mock, context, createRouter, post, get, put } = getCoreMock(); @@ -56,7 +99,7 @@ describe('createApi', () => { .add(() => ({ endpoint: 'GET /foo', options: { tags: ['access:apm'] }, - handler: async () => null, + handler: async () => ({}), })) .add(() => ({ endpoint: 'POST /bar', @@ -64,21 +107,21 @@ describe('createApi', () => { body: t.string, }), options: { tags: ['access:apm'] }, - handler: async () => null, + handler: async () => ({}), })) .add(() => ({ endpoint: 'PUT /baz', options: { tags: ['access:apm', 'access:apm_write'], }, - handler: async () => null, + handler: async () => ({}), })) .add({ endpoint: 'GET /qux', options: { tags: ['access:apm', 'access:apm_write'], }, - handler: async () => null, + handler: async () => ({}), }) .init(mock, context); @@ -122,102 +165,78 @@ describe('createApi', () => { }); describe('when validating', () => { - const initApi = (params?: RouteParamsRT) => { - const { mock, context, createRouter, get, post } = getCoreMock(); - const handlerMock = jest.fn(); - createApi() - .add(() => ({ - endpoint: 'GET /foo', - params, - options: { tags: ['access:apm'] }, - handler: handlerMock, - })) - .init(mock, context); - - const routeHandler = get.mock.calls[0][1]; - - const responseMock = { - ok: jest.fn(), - internalError: jest.fn(), - notFound: jest.fn(), - forbidden: jest.fn(), - badRequest: jest.fn(), - }; - - const simulate = (requestMock: any) => { - return routeHandler( - {}, - { - // stub default values - params: {}, - query: {}, - body: null, - ...requestMock, - }, - responseMock - ); - }; - - return { simulate, handlerMock, createRouter, get, post, responseMock }; - }; - - it('adds a _debug query parameter by default', async () => { - const { simulate, handlerMock, responseMock } = initApi(); - - await simulate({ query: { _debug: 'true' } }); + describe('_inspect', () => { + it('allows _inspect=true', async () => { + const { simulateRequest, handlerMock, responseMock } = initApi(); + await simulateRequest({ query: { _inspect: 'true' } }); + + const params = handlerMock.mock.calls[0][0].context.params; + expect(params).toEqual({ query: { _inspect: true } }); + expect(handlerMock).toHaveBeenCalledTimes(1); + + // responds with ok + expect(responseMock.custom).not.toHaveBeenCalled(); + expect(responseMock.ok).toHaveBeenCalledWith({ + body: { _inspect: [] }, + }); + }); - expect(responseMock.badRequest).not.toHaveBeenCalled(); + it('rejects _inspect=1', async () => { + const { simulateRequest, responseMock } = initApi(); + await simulateRequest({ query: { _inspect: 1 } }); + + // responds with error handler + expect(responseMock.ok).not.toHaveBeenCalled(); + expect(responseMock.custom).toHaveBeenCalledWith({ + body: { + attributes: { _inspect: [] }, + message: + 'Invalid value 1 supplied to : strict_keys/query: Partial<{| _inspect: pipe(JSON, boolean) |}>/_inspect: pipe(JSON, boolean)', + }, + statusCode: 400, + }); + }); - expect(handlerMock).toHaveBeenCalledTimes(1); + it('allows omitting _inspect', async () => { + const { simulateRequest, handlerMock, responseMock } = initApi(); + await simulateRequest({ query: {} }); - expect(responseMock.ok).toHaveBeenCalled(); + const params = handlerMock.mock.calls[0][0].context.params; + expect(params).toEqual({ query: { _inspect: false } }); + expect(handlerMock).toHaveBeenCalledTimes(1); - const params = handlerMock.mock.calls[0][0].context.params; - - expect(params).toEqual({ - query: { - _debug: true, - }, + // responds with ok + expect(responseMock.custom).not.toHaveBeenCalled(); + expect(responseMock.ok).toHaveBeenCalledWith({ body: {} }); }); - - await simulate({ - query: { - _debug: 1, - }, - }); - - expect(responseMock.badRequest).toHaveBeenCalled(); }); - it('throws if any parameters are used but no types are defined', async () => { - const { simulate, responseMock } = initApi(); + it('throws if unknown parameters are provided', async () => { + const { simulateRequest, responseMock } = initApi(); - await simulate({ - query: { - _debug: true, - extra: '', - }, + await simulateRequest({ + query: { _inspect: true, extra: '' }, }); - expect(responseMock.badRequest).toHaveBeenCalledTimes(1); + expect(responseMock.custom).toHaveBeenCalledTimes(1); - await simulate({ + await simulateRequest({ body: { foo: 'bar' }, }); - expect(responseMock.badRequest).toHaveBeenCalledTimes(2); + expect(responseMock.custom).toHaveBeenCalledTimes(2); - await simulate({ + await simulateRequest({ params: { foo: 'bar', }, }); - expect(responseMock.badRequest).toHaveBeenCalledTimes(3); + expect(responseMock.custom).toHaveBeenCalledTimes(3); }); it('validates path parameters', async () => { - const { simulate, handlerMock, responseMock } = initApi( + const { simulateRequest, handlerMock, responseMock } = initApi( t.type({ path: t.type({ foo: t.string, @@ -225,7 +244,7 @@ describe('createApi', () => { }) ); - await simulate({ + await simulateRequest({ params: { foo: 'bar', }, @@ -234,7 +253,7 @@ describe('createApi', () => { expect(handlerMock).toHaveBeenCalledTimes(1); expect(responseMock.ok).toHaveBeenCalledTimes(1); - expect(responseMock.badRequest).not.toHaveBeenCalled(); + expect(responseMock.custom).not.toHaveBeenCalled(); const params = handlerMock.mock.calls[0][0].context.params; @@ -243,48 +262,48 @@ describe('createApi', () => { foo: 'bar', }, query: { - _debug: false, + _inspect: false, }, }); - await simulate({ + await simulateRequest({ params: { bar: 'foo', }, }); - expect(responseMock.badRequest).toHaveBeenCalledTimes(1); + expect(responseMock.custom).toHaveBeenCalledTimes(1); - await simulate({ + await simulateRequest({ params: { foo: 9, }, }); - expect(responseMock.badRequest).toHaveBeenCalledTimes(2); + expect(responseMock.custom).toHaveBeenCalledTimes(2); - await simulate({ + await simulateRequest({ params: { foo: 'bar', extra: '', }, }); - expect(responseMock.badRequest).toHaveBeenCalledTimes(3); + expect(responseMock.custom).toHaveBeenCalledTimes(3); }); it('validates body parameters', async () => { - const { simulate, handlerMock, responseMock } = initApi( + const { simulateRequest, handlerMock, responseMock } = initApi( t.type({ body: t.string, }) ); - await simulate({ + await simulateRequest({ body: '', }); - expect(responseMock.badRequest).not.toHaveBeenCalled(); + expect(responseMock.custom).not.toHaveBeenCalled(); expect(handlerMock).toHaveBeenCalledTimes(1); expect(responseMock.ok).toHaveBeenCalledTimes(1); @@ -293,19 +312,19 @@ describe('createApi', () => { expect(params).toEqual({ body: '', query: { - _debug: false, + _inspect: false, }, }); - await simulate({ + await simulateRequest({ body: null, }); - expect(responseMock.badRequest).toHaveBeenCalledTimes(1); + expect(responseMock.custom).toHaveBeenCalledTimes(1); }); it('validates query parameters', async () => { - const { simulate, handlerMock, responseMock } = initApi( + const { simulateRequest, handlerMock, responseMock } = initApi( t.type({ query: t.type({ bar: t.string, @@ -314,15 +333,15 @@ describe('createApi', () => { }) ); - await simulate({ + await simulateRequest({ query: { bar: '', - _debug: 'true', + _inspect: 'true', filterNames: JSON.stringify(['hostName', 'agentName']), }, }); - expect(responseMock.badRequest).not.toHaveBeenCalled(); + expect(responseMock.custom).not.toHaveBeenCalled(); expect(handlerMock).toHaveBeenCalledTimes(1); expect(responseMock.ok).toHaveBeenCalledTimes(1); @@ -331,19 +350,19 @@ describe('createApi', () => { expect(params).toEqual({ query: { bar: '', - _debug: true, + _inspect: true, filterNames: ['hostName', 'agentName'], }, }); - await simulate({ + await simulateRequest({ query: { bar: '', foo: '', }, }); - expect(responseMock.badRequest).toHaveBeenCalledTimes(1); + expect(responseMock.custom).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/apm/server/routes/create_api/index.ts b/x-pack/plugins/apm/server/routes/create_api/index.ts index 46f2628cc73d5..13e70a2043cf0 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.ts @@ -11,19 +11,20 @@ import { schema } from '@kbn/config-schema'; import * as t from 'io-ts'; import { PathReporter } from 'io-ts/lib/PathReporter'; import { isLeft } from 'fp-ts/lib/Either'; -import { KibanaResponseFactory, RouteRegistrar } from 'src/core/server'; +import { KibanaRequest, RouteRegistrar } from 'src/core/server'; import { RequestAbortedError } from '@elastic/elasticsearch/lib/errors'; import agent from 'elastic-apm-node'; +import { parseMethod } from '../../../common/apm_api/parse_endpoint'; import { merge } from '../../../common/runtime_types/merge'; import { strictKeysRt } from '../../../common/runtime_types/strict_keys_rt'; import { APMConfig } from '../..'; -import { ServerAPI } from '../typings'; +import { InspectResponse, RouteParamsRT, ServerAPI } from '../typings'; import { jsonRt } from '../../../common/runtime_types/json_rt'; import type { ApmPluginRequestHandlerContext } from '../typings'; -const debugRt = t.exact( +const inspectRt = t.exact( t.partial({ - query: t.exact(t.partial({ _debug: jsonRt.pipe(t.boolean) })), + query: t.exact(t.partial({ _inspect: jsonRt.pipe(t.boolean) })), }) ); @@ -32,6 +33,11 @@ type RouteOrRouteFactoryFn = Parameters['add']>[0]; const isNotEmpty = (val: any) => val !== undefined && val !== null && !(isPlainObject(val) && isEmpty(val)); +export const inspectableEsQueriesMap = new WeakMap< + KibanaRequest, + InspectResponse +>(); + export function createApi() { const routes: RouteOrRouteFactoryFn[] = []; const api: ServerAPI<{}> = { @@ -58,24 +64,10 @@ export function createApi() { const { params, endpoint, options, handler } = route; const [method, path] = endpoint.split(' '); - - const typedRouterMethod = method.trim().toLowerCase() as - | 'get' - | 'post' - | 'put' - | 'delete'; - - if (!['get', 'post', 'put', 'delete'].includes(typedRouterMethod)) { - throw new Error( - "Couldn't register route, as endpoint was not prefixed with a valid HTTP method" - ); - } + const typedRouterMethod = parseMethod(method); // For all runtime types with props, we create an exact // version that will strip all keys that are unvalidated. - - const paramsRt = params ? merge([params, debugRt]) : debugRt; - const anyObject = schema.object({}, { unknowns: 'allow' }); (router[typedRouterMethod] as RouteRegistrar< @@ -102,56 +94,52 @@ export function createApi() { }); } - try { - const paramMap = pickBy( - { - path: request.params, - body: request.body, - query: { - _debug: 'false', - ...request.query, - }, - }, - isNotEmpty - ); - - const result = strictKeysRt(paramsRt).decode(paramMap); + // init debug queries + inspectableEsQueriesMap.set(request, []); - if (isLeft(result)) { - throw Boom.badRequest(PathReporter.report(result)[0]); - } + try { + const validParams = validateParams(request, params); const data = await handler({ request, context: { ...context, plugins, - // Only return values for parameters that have runtime types, - // but always include query as _debug is always set even if - // it's not defined in the route. - params: mergeLodash( - { query: { _debug: false } }, - pickBy(result.right, isNotEmpty) - ), + params: validParams, config, logger, }, }); - return response.ok({ body: data as any }); + const body = { ...data }; + if (validParams.query._inspect) { + body._inspect = inspectableEsQueriesMap.get(request); + } + + // cleanup + inspectableEsQueriesMap.delete(request); + + return response.ok({ body }); } catch (error) { + const opts = { + statusCode: 500, + body: { + message: error.message, + attributes: { + _inspect: inspectableEsQueriesMap.get(request), + }, + }, + }; + if (Boom.isBoom(error)) { - return convertBoomToKibanaResponse(error, response); + opts.statusCode = error.output.statusCode; } if (error instanceof RequestAbortedError) { - return response.custom({ - statusCode: 499, - body: { - message: 'Client closed request', - }, - }); + opts.statusCode = 499; + opts.body.message = 'Client closed request'; } - throw error; + + return response.custom(opts); } } ); @@ -162,22 +150,35 @@ export function createApi() { return api; } -function convertBoomToKibanaResponse( - error: Boom.Boom, - response: KibanaResponseFactory +function validateParams( + request: KibanaRequest, + params: RouteParamsRT | undefined ) { - const opts = { body: { message: error.message } }; - switch (error.output.statusCode) { - case 404: - return response.notFound(opts); - - case 400: - return response.badRequest(opts); + const paramsRt = params ? merge([params, inspectRt]) : inspectRt; + const paramMap = pickBy( + { + path: request.params, + body: request.body, + query: { + _inspect: 'false', + // @ts-ignore + ...request.query, + }, + }, + isNotEmpty + ); - case 403: - return response.forbidden(opts); + const result = strictKeysRt(paramsRt).decode(paramMap); - default: - throw error; + if (isLeft(result)) { + throw Boom.badRequest(PathReporter.report(result)[0]); } + + // Only return values for parameters that have runtime types, + // but always include query as _inspect is always set even if + // it's not defined in the route. + return mergeLodash( + { query: { _inspect: false } }, + pickBy(result.right, isNotEmpty) + ); } diff --git a/x-pack/plugins/apm/server/routes/create_route.ts b/x-pack/plugins/apm/server/routes/create_route.ts index 4d30e706cdd5c..d74aac0992eb4 100644 --- a/x-pack/plugins/apm/server/routes/create_route.ts +++ b/x-pack/plugins/apm/server/routes/create_route.ts @@ -6,20 +6,20 @@ */ import { CoreSetup } from 'src/core/server'; -import { Route, RouteParamsRT } from './typings'; +import { HandlerReturn, Route, RouteParamsRT } from './typings'; export function createRoute< TEndpoint extends string, - TRouteParamsRT extends RouteParamsRT | undefined = undefined, - TReturn = unknown + TReturn extends HandlerReturn, + TRouteParamsRT extends RouteParamsRT | undefined = undefined >( route: Route ): Route; export function createRoute< TEndpoint extends string, - TRouteParamsRT extends RouteParamsRT | undefined = undefined, - TReturn = unknown + TReturn extends HandlerReturn, + TRouteParamsRT extends RouteParamsRT | undefined = undefined >( route: (core: CoreSetup) => Route ): (core: CoreSetup) => Route; diff --git a/x-pack/plugins/apm/server/routes/environments.ts b/x-pack/plugins/apm/server/routes/environments.ts index 448591f7e143f..4aa7d7e6d412f 100644 --- a/x-pack/plugins/apm/server/routes/environments.ts +++ b/x-pack/plugins/apm/server/routes/environments.ts @@ -30,10 +30,12 @@ export const environmentsRoute = createRoute({ setup ); - return getEnvironments({ + const environments = await getEnvironments({ setup, serviceName, searchAggregatedTransactions, }); + + return { environments }; }, }); diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts index 710e614165aa5..f69d3fc9631d1 100644 --- a/x-pack/plugins/apm/server/routes/errors.ts +++ b/x-pack/plugins/apm/server/routes/errors.ts @@ -36,7 +36,7 @@ export const errorsRoute = createRoute({ const { serviceName } = params.path; const { environment, kuery, sortField, sortDirection } = params.query; - return getErrorGroups({ + const errorGroups = await getErrorGroups({ environment, kuery, serviceName, @@ -44,6 +44,8 @@ export const errorsRoute = createRoute({ sortDirection, setup, }); + + return { errorGroups }; }, }); diff --git a/x-pack/plugins/apm/server/routes/index_pattern.ts b/x-pack/plugins/apm/server/routes/index_pattern.ts index ed1354a219164..fd7d2120ab6f5 100644 --- a/x-pack/plugins/apm/server/routes/index_pattern.ts +++ b/x-pack/plugins/apm/server/routes/index_pattern.ts @@ -21,10 +21,13 @@ export const staticIndexPatternRoute = createRoute((core) => ({ getInternalSavedObjectsClient(core), ]); - await createStaticIndexPattern(setup, context, savedObjectsClient); + const didCreateIndexPattern = await createStaticIndexPattern( + setup, + context, + savedObjectsClient + ); - // send empty response regardless of outcome - return undefined; + return { created: didCreateIndexPattern }; }, })); @@ -41,6 +44,8 @@ export const apmIndexPatternTitleRoute = createRoute({ endpoint: 'GET /api/apm/index_pattern/title', options: { tags: ['access:apm'] }, handler: async ({ context }) => { - return getApmIndexPatternTitle(context); + return { + indexPatternTitle: getApmIndexPatternTitle(context), + }; }, }); diff --git a/x-pack/plugins/apm/server/routes/observability_overview.ts b/x-pack/plugins/apm/server/routes/observability_overview.ts index 1a1fa799639bc..b9c0a76b6fb90 100644 --- a/x-pack/plugins/apm/server/routes/observability_overview.ts +++ b/x-pack/plugins/apm/server/routes/observability_overview.ts @@ -9,7 +9,7 @@ import * as t from 'io-ts'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceCount } from '../lib/observability_overview/get_service_count'; import { getTransactionCoordinates } from '../lib/observability_overview/get_transaction_coordinates'; -import { hasData } from '../lib/observability_overview/has_data'; +import { getHasData } from '../lib/observability_overview/has_data'; import { createRoute } from './create_route'; import { rangeRt } from './default_api_types'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; @@ -20,7 +20,8 @@ export const observabilityOverviewHasDataRoute = createRoute({ options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return await hasData({ setup }); + const res = await getHasData({ setup }); + return { hasData: res }; }, }); diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index ecf56e2aec246..3156acb469a72 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -79,12 +79,14 @@ export const rumPageLoadDistributionRoute = createRoute({ query: { minPercentile, maxPercentile, urlQuery }, } = context.params; - return getPageLoadDistribution({ + const pageLoadDistribution = await getPageLoadDistribution({ setup, minPercentile, maxPercentile, urlQuery, }); + + return { pageLoadDistribution }; }, }); @@ -105,13 +107,15 @@ export const rumPageLoadDistBreakdownRoute = createRoute({ query: { minPercentile, maxPercentile, breakdown, urlQuery }, } = context.params; - return getPageLoadDistBreakdown({ + const pageLoadDistBreakdown = await getPageLoadDistBreakdown({ setup, minPercentile: Number(minPercentile), maxPercentile: Number(maxPercentile), breakdown, urlQuery, }); + + return { pageLoadDistBreakdown }; }, }); @@ -145,7 +149,8 @@ export const rumServicesRoute = createRoute({ handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return getRumServices({ setup }); + const rumServices = await getRumServices({ setup }); + return { rumServices }; }, }); @@ -322,12 +327,14 @@ function createLocalFiltersRoute< setup, }); - return getLocalUIFilters({ + const localUiFilters = await getLocalUIFilters({ projection, setup, uiFilters, localFilterNames: filterNames, }); + + return { localUiFilters }; }, }); } diff --git a/x-pack/plugins/apm/server/routes/service_nodes.ts b/x-pack/plugins/apm/server/routes/service_nodes.ts index e65b0b679da5a..e9060688c63a6 100644 --- a/x-pack/plugins/apm/server/routes/service_nodes.ts +++ b/x-pack/plugins/apm/server/routes/service_nodes.ts @@ -26,10 +26,7 @@ export const serviceNodesRoute = createRoute({ const { serviceName } = params.path; const { kuery } = params.query; - return getServiceNodes({ - kuery, - setup, - serviceName, - }); + const serviceNodes = await getServiceNodes({ kuery, setup, serviceName }); + return { serviceNodes }; }, }); diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 7ba19035a90b0..b4d25ca8b2a06 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -56,15 +56,13 @@ export const servicesRoute = createRoute({ setup ); - const services = await getServices({ + return getServices({ environment, kuery, setup, searchAggregatedTransactions, logger: context.logger, }); - - return services; }, }); @@ -465,7 +463,7 @@ export const serviceInstancesPrimaryStatisticsRoute = createRoute({ const { start, end } = setup; - return getServiceInstancesPrimaryStatistics({ + const serviceInstances = await getServiceInstancesPrimaryStatistics({ environment, kuery, latencyAggregationType, @@ -476,6 +474,8 @@ export const serviceInstancesPrimaryStatisticsRoute = createRoute({ start, end, }); + + return { serviceInstances }; }, }); @@ -558,12 +558,14 @@ export const serviceDependenciesRoute = createRoute({ const { serviceName } = context.params.path; const { environment, numBuckets } = context.params.query; - return getServiceDependencies({ + const serviceDependencies = await getServiceDependencies({ serviceName, environment, setup, numBuckets, }); + + return { serviceDependencies }; }, }); @@ -586,12 +588,14 @@ export const serviceProfilingTimelineRoute = createRoute({ query: { environment, kuery }, } = context.params; - return getServiceProfilingTimeline({ + const profilingTimeline = await getServiceProfilingTimeline({ kuery, setup, serviceName, environment, }); + + return { profilingTimeline }; }, }); diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts index e3ed398171d01..31e8d6cc1e9f0 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -31,7 +31,8 @@ export const agentConfigurationRoute = createRoute({ options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return await listConfigurations({ setup }); + const configurations = await listConfigurations({ setup }); + return { configurations }; }, }); @@ -204,10 +205,12 @@ export const listAgentConfigurationServicesRoute = createRoute({ const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); - return await getServiceNames({ + const serviceNames = await getServiceNames({ setup, searchAggregatedTransactions, }); + + return { serviceNames }; }, }); @@ -225,11 +228,13 @@ export const listAgentConfigurationEnvironmentsRoute = createRoute({ setup ); - return await getEnvironments({ + const environments = await getEnvironments({ serviceName, setup, searchAggregatedTransactions, }); + + return { environments }; }, }); diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index e5922d9ed3e94..de7f35c4081bc 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -71,6 +71,8 @@ export const createAnomalyDetectionJobsRoute = createRoute({ licensingPlugin: context.licensing, featureName: 'ml', }); + + return { jobCreated: true }; }, }); @@ -85,10 +87,12 @@ export const anomalyDetectionEnvironmentsRoute = createRoute({ setup ); - return await getAllEnvironments({ + const environments = await getAllEnvironments({ setup, searchAggregatedTransactions, includeMissing: true, }); + + return { environments }; }, }); diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts index 0d47579f50aec..91057c97579e4 100644 --- a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts +++ b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts @@ -18,7 +18,8 @@ export const apmIndexSettingsRoute = createRoute({ endpoint: 'GET /api/apm/settings/apm-index-settings', options: { tags: ['access:apm'] }, handler: async ({ context }) => { - return await getApmIndexSettings({ context }); + const apmIndexSettings = await getApmIndexSettings({ context }); + return { apmIndexSettings }; }, }); diff --git a/x-pack/plugins/apm/server/routes/settings/custom_link.ts b/x-pack/plugins/apm/server/routes/settings/custom_link.ts index fc217bef772d0..a6ab553f09419 100644 --- a/x-pack/plugins/apm/server/routes/settings/custom_link.ts +++ b/x-pack/plugins/apm/server/routes/settings/custom_link.ts @@ -52,7 +52,8 @@ export const listCustomLinksRoute = createRoute({ const { query } = context.params; // picks only the items listed in FILTER_OPTIONS const filters = pick(query, FILTER_OPTIONS); - return await listCustomLinks({ setup, filters }); + const customLinks = await listCustomLinks({ setup, filters }); + return { customLinks }; }, }); diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 4d3e07040f76b..1575041fb2f45 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -13,7 +13,7 @@ import { Logger, } from 'src/core/server'; import { Observable } from 'rxjs'; -import { RequiredKeys } from 'utility-types'; +import { RequiredKeys, DeepPartial } from 'utility-types'; import { ObservabilityPluginSetup } from '../../../observability/server'; import { LicensingApiRequestHandlerContext } from '../../../licensing/server'; import { SecurityPluginSetup } from '../../../security/server'; @@ -21,6 +21,20 @@ import { MlPluginSetup } from '../../../ml/server'; import { FetchOptions } from '../../common/fetch_options'; import { APMConfig } from '..'; +export type HandlerReturn = Record; + +interface InspectQueryParam { + query: { _inspect: boolean }; +} + +export type InspectResponse = Array<{ + response: any; + duration: number; + requestType: string; + requestParams: Record; + esError: Error; +}>; + export interface RouteParams { path?: Record; query?: Record; @@ -36,15 +50,14 @@ export type RouteParamsRT = WithoutIncompatibleMethods>; export type RouteHandler< TParamsRT extends RouteParamsRT | undefined, - TReturn + TReturn extends HandlerReturn > = (kibanaContext: { context: APMRequestHandlerContext< - (TParamsRT extends RouteParamsRT ? t.TypeOf : {}) & { - query: { _debug: boolean }; - } + (TParamsRT extends RouteParamsRT ? t.TypeOf : {}) & + InspectQueryParam >; request: KibanaRequest; -}) => Promise; +}) => Promise; interface RouteOptions { tags: Array< @@ -58,7 +71,7 @@ interface RouteOptions { export interface Route< TEndpoint extends string, TRouteParamsRT extends RouteParamsRT | undefined, - TReturn + TReturn extends HandlerReturn > { endpoint: TEndpoint; options: RouteOptions; @@ -76,7 +89,7 @@ export interface ApmPluginRequestHandlerContext extends RequestHandlerContext { export type APMRequestHandlerContext< TRouteParams = {} > = ApmPluginRequestHandlerContext & { - params: TRouteParams & { query: { _debug: boolean } }; + params: TRouteParams & InspectQueryParam; config: APMConfig; logger: Logger; plugins: { @@ -97,8 +110,8 @@ export interface ServerAPI { _S: TRouteState; add< TEndpoint extends string, - TRouteParamsRT extends RouteParamsRT | undefined = undefined, - TReturn = unknown + TReturn extends HandlerReturn, + TRouteParamsRT extends RouteParamsRT | undefined = undefined >( route: | Route @@ -108,7 +121,7 @@ export interface ServerAPI { { [key in TEndpoint]: { params: TRouteParamsRT; - ret: TReturn; + ret: TReturn & { _inspect?: InspectResponse }; }; } >; @@ -132,6 +145,16 @@ type MaybeOptional }> = RequiredKeys< ? { params?: T['params'] } : { params: T['params'] }; +export type MaybeParams< + TRouteState, + TEndpoint extends keyof TRouteState & string +> = TRouteState[TEndpoint] extends { params: t.Any } + ? MaybeOptional<{ + params: t.OutputOf & + DeepPartial; + }> + : {}; + export type Client< TRouteState, TOptions extends { abortable: boolean } = { abortable: true } @@ -142,9 +165,7 @@ export type Client< > & { forceCache?: boolean; endpoint: TEndpoint; - } & (TRouteState[TEndpoint] extends { params: t.Any } - ? MaybeOptional<{ params: t.OutputOf }> - : {}) & + } & MaybeParams & (TOptions extends { abortable: true } ? { signal: AbortSignal | null } : {}) ) => Promise< TRouteState[TEndpoint] extends { ret: any } diff --git a/x-pack/plugins/observability/common/ui_settings_keys.ts b/x-pack/plugins/observability/common/ui_settings_keys.ts index 05abac80b67ce..cb6ea799078a2 100644 --- a/x-pack/plugins/observability/common/ui_settings_keys.ts +++ b/x-pack/plugins/observability/common/ui_settings_keys.ts @@ -6,3 +6,4 @@ */ export const enableAlertingExperience = 'observability:enableAlertingExperience'; +export const enableInspectEsQueries = 'observability:enableInspectEsQueries'; diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index f473ed963c75a..35443ca090077 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -19,6 +19,7 @@ export type { ObservabilityPublicPluginsSetup, ObservabilityPublicPluginsStart, }; +export { enableInspectEsQueries } from '../common/ui_settings_keys'; export const plugin: PluginInitializer< ObservabilityPublicSetup, diff --git a/x-pack/plugins/observability/server/ui_settings.ts b/x-pack/plugins/observability/server/ui_settings.ts index 3123ce96114d7..43041280d0282 100644 --- a/x-pack/plugins/observability/server/ui_settings.ts +++ b/x-pack/plugins/observability/server/ui_settings.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { UiSettingsParams } from '../../../../src/core/types'; -import { enableAlertingExperience } from '../common/ui_settings_keys'; +import { enableAlertingExperience, enableInspectEsQueries } from '../common/ui_settings_keys'; /** * uiSettings definitions for Observability. @@ -29,4 +29,15 @@ export const uiSettings: Record> = { ), schema: schema.boolean(), }, + [enableInspectEsQueries]: { + category: ['observability'], + name: i18n.translate('xpack.observability.enableInspectEsQueriesExperimentName', { + defaultMessage: 'inspect ES queries', + }), + value: false, + description: i18n.translate('xpack.observability.enableInspectEsQueriesExperimentDescription', { + defaultMessage: 'Inspect Elasticsearch queries in API responses.', + }), + schema: schema.boolean(), + }, }; diff --git a/x-pack/plugins/uptime/public/state/api/utils.ts b/x-pack/plugins/uptime/public/state/api/utils.ts index 2b310a5241a62..f59f1939b5989 100644 --- a/x-pack/plugins/uptime/public/state/api/utils.ts +++ b/x-pack/plugins/uptime/public/state/api/utils.ts @@ -67,7 +67,7 @@ class ApiService { const response = await this._http!.fetch({ path: apiUrl, - query: { ...params, ...(debugEnabled ? { _debug: true } : {}) }, + query: { ...params, ...(debugEnabled ? { _inspect: true } : {}) }, asResponse, }); diff --git a/x-pack/plugins/uptime/server/lib/lib.ts b/x-pack/plugins/uptime/server/lib/lib.ts index 4b78683444297..a91ff3d3b0faf 100644 --- a/x-pack/plugins/uptime/server/lib/lib.ts +++ b/x-pack/plugins/uptime/server/lib/lib.ts @@ -51,7 +51,7 @@ export function createUptimeESClient({ request?: KibanaRequest; savedObjectsClient: SavedObjectsClientContract | ISavedObjectsRepository; }) { - const { _debug = false } = (request?.query as { _debug: boolean }) ?? {}; + const { _inspect = false } = (request?.query as { _inspect: boolean }) ?? {}; return { baseESClient: esClient, @@ -72,7 +72,7 @@ export function createUptimeESClient({ } catch (e) { esError = e; } - if (_debug && request) { + if (_inspect && request) { debugESCall({ startTime, request, esError, operationName: 'search', params: esParams }); } @@ -99,7 +99,7 @@ export function createUptimeESClient({ esError = e; } - if (_debug && request) { + if (_inspect && request) { debugESCall({ startTime, request, esError, operationName: 'count', params: esParams }); } diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts index a7e02adc0401a..29a7a06f1530a 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts @@ -15,7 +15,7 @@ export const createGetIndexStatusRoute: UMRestApiRouteFactory = (libs: UMServerL path: API_URLS.INDEX_STATUS, validate: { query: schema.object({ - _debug: schema.maybe(schema.boolean()), + _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts index 8e98468496952..491d20b929d2b 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts @@ -21,7 +21,7 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({ statusFilter: schema.maybe(schema.string()), query: schema.maybe(schema.string()), pageSize: schema.number(), - _debug: schema.maybe(schema.boolean()), + _inspect: schema.maybe(schema.boolean()), }), }, options: { diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts index 13ff3d3be3c0a..77f265d0b81e8 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts @@ -18,7 +18,7 @@ export const createGetMonitorLocationsRoute: UMRestApiRouteFactory = (libs: UMSe monitorId: schema.string(), dateStart: schema.string(), dateEnd: schema.string(), - _debug: schema.maybe(schema.boolean()), + _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts index 3663c42ad4ab8..94b50386ac216 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts @@ -18,7 +18,7 @@ export const createGetStatusBarRoute: UMRestApiRouteFactory = (libs: UMServerLib monitorId: schema.string(), dateStart: schema.string(), dateEnd: schema.string(), - _debug: schema.maybe(schema.boolean()), + _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts index 6144825de1a7a..eefde90677312 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts @@ -18,7 +18,7 @@ export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServ monitorId: schema.string(), dateStart: schema.maybe(schema.string()), dateEnd: schema.maybe(schema.string()), - _debug: schema.maybe(schema.boolean()), + _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, context, request }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts index cc2647c347b3f..e94198ee4e063 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts @@ -19,7 +19,7 @@ export const createGetMonitorDurationRoute: UMRestApiRouteFactory = (libs: UMSer monitorId: schema.string(), dateStart: schema.string(), dateEnd: schema.string(), - _debug: schema.maybe(schema.boolean()), + _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts index 5414f45e29098..354c57c365115 100644 --- a/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts +++ b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts @@ -27,7 +27,7 @@ export const createGetOverviewFilters: UMRestApiRouteFactory = (libs: UMServerLi schemes: arrayOrStringType, ports: arrayOrStringType, tags: arrayOrStringType, - _debug: schema.maybe(schema.boolean()), + _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request, response }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts index 69de7eba6e756..db111390cfaf7 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts @@ -21,7 +21,7 @@ export const createGetPingHistogramRoute: UMRestApiRouteFactory = (libs: UMServe filters: schema.maybe(schema.string()), bucketSize: schema.maybe(schema.string()), query: schema.maybe(schema.string()), - _debug: schema.maybe(schema.boolean()), + _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts index 04f4e6cd3c18e..0178fd770f9ce 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts @@ -23,7 +23,7 @@ export const createGetPingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) = size: schema.maybe(schema.number()), sort: schema.maybe(schema.string()), status: schema.maybe(schema.string()), - _debug: schema.maybe(schema.boolean()), + _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request, response }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts index 2b056498d7f10..ab8a01cfb9c3f 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts @@ -16,10 +16,10 @@ export const createJourneyScreenshotRoute: UMRestApiRouteFactory = (libs: UMServ params: schema.object({ checkGroup: schema.string(), stepIndex: schema.number(), - _debug: schema.maybe(schema.boolean()), + _inspect: schema.maybe(schema.boolean()), }), query: schema.object({ - _debug: schema.maybe(schema.boolean()), + _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request, response }) => { diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts b/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts index 9b5bffc380c27..31555be25b2ff 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts @@ -22,7 +22,7 @@ export const createJourneyRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => syntheticEventTypes: schema.maybe( schema.oneOf([schema.arrayOf(schema.string()), schema.string()]) ), - _debug: schema.maybe(schema.boolean()), + _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { @@ -55,7 +55,7 @@ export const createJourneyFailedStepsRoute: UMRestApiRouteFactory = (libs: UMSer validate: { query: schema.object({ checkGroups: schema.arrayOf(schema.string()), - _debug: schema.maybe(schema.boolean()), + _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts index 8c80c4d512b56..67b106fdf6814 100644 --- a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts +++ b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts @@ -19,7 +19,7 @@ export const createGetSnapshotCount: UMRestApiRouteFactory = (libs: UMServerLibs dateRangeEnd: schema.string(), filters: schema.maybe(schema.string()), query: schema.maybe(schema.string()), - _debug: schema.maybe(schema.boolean()), + _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics/last_successful_step.ts b/x-pack/plugins/uptime/server/rest_api/synthetics/last_successful_step.ts index a1523fae9d4a1..c326037b9ecbf 100644 --- a/x-pack/plugins/uptime/server/rest_api/synthetics/last_successful_step.ts +++ b/x-pack/plugins/uptime/server/rest_api/synthetics/last_successful_step.ts @@ -17,7 +17,7 @@ export const createLastSuccessfulStepRoute: UMRestApiRouteFactory = (libs: UMSer monitorId: schema.string(), stepIndex: schema.number(), timestamp: schema.string(), - _debug: schema.maybe(schema.boolean()), + _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request, response }) => { diff --git a/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts b/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts index 391770120c49c..ef1e0a07c6392 100644 --- a/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts +++ b/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts @@ -22,7 +22,7 @@ export const createLogPageViewRoute: UMRestApiRouteFactory = () => ({ autoRefreshEnabled: schema.boolean(), autorefreshInterval: schema.number(), refreshTelemetryHistory: schema.maybe(schema.boolean()), - _debug: schema.maybe(schema.boolean()), + _inspect: schema.maybe(schema.boolean()), }), }, handler: async ({ savedObjectsClient, uptimeEsClient, request }): Promise => { diff --git a/x-pack/test/apm_api_integration/common/apm_api_supertest.ts b/x-pack/test/apm_api_integration/common/apm_api_supertest.ts new file mode 100644 index 0000000000000..76eab7ab85cf1 --- /dev/null +++ b/x-pack/test/apm_api_integration/common/apm_api_supertest.ts @@ -0,0 +1,50 @@ +/* + * 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 { format } from 'url'; +import supertest from 'supertest'; +import { MaybeParams } from '../../../plugins/apm/server/routes/typings'; +import { parseEndpoint } from '../../../plugins/apm/common/apm_api/parse_endpoint'; +import { APMAPI } from '../../../plugins/apm/server/routes/create_apm_api'; +import type { APIReturnType } from '../../../plugins/apm/public/services/rest/createCallApmApi'; + +export function createApmApiSupertest(st: supertest.SuperTest) { + return async ( + options: { + endpoint: TPath; + } & MaybeParams + ): Promise<{ + status: number; + body: APIReturnType; + }> => { + const { endpoint } = options; + + // @ts-expect-error + const params = 'params' in options ? options.params : {}; + + const { method, pathname } = parseEndpoint(endpoint, params?.path); + const url = format({ pathname, query: params?.query }); + + const res = params.body + ? await st[method](url).send(params.body).set('kbn-xsrf', 'foo') + : await st[method](url).set('kbn-xsrf', 'foo'); + + // supertest doesn't throw on http errors + if (res.status !== 200) { + const e = new Error( + `Unhandled ApmApiSupertest error. Status: "${ + res.status + }". Endpoint: "${endpoint}". ${JSON.stringify(res.body)}` + ); + // @ts-expect-error + e.res = res; + throw e; + } + + return res; + }; +} diff --git a/x-pack/test/apm_api_integration/common/config.ts b/x-pack/test/apm_api_integration/common/config.ts index adf6fcfbe9818..04ce83323ee66 100644 --- a/x-pack/test/apm_api_integration/common/config.ts +++ b/x-pack/test/apm_api_integration/common/config.ts @@ -6,7 +6,7 @@ */ import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; -import supertestAsPromised from 'supertest-as-promised'; +import supertest from 'supertest'; import { format, UrlObject } from 'url'; import path from 'path'; import { InheritedFtrProviderContext, InheritedServices } from './ftr_provider_context'; @@ -33,7 +33,7 @@ const supertestAsApmUser = (kibanaServer: UrlObject, apmUser: ApmUser) => async auth: `${apmUser}:${APM_TEST_PASSWORD}`, }); - return supertestAsPromised(url); + return supertest(url); }; export function createTestConfig(config: Config) { diff --git a/x-pack/test/apm_api_integration/tests/alerts/chart_preview.ts b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.ts index 58da2e2b0df5b..3712b49ce1696 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/chart_preview.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.ts @@ -6,66 +6,112 @@ */ import expect from '@kbn/expect'; -import { format } from 'url'; +import { createApmApiSupertest } from '../../common/apm_api_supertest'; import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const apmApiSupertest = createApmApiSupertest(getService('supertest')); const archiveName = 'apm_8.0.0'; const { end } = archives[archiveName]; const start = new Date(Date.parse(end) - 600000).toISOString(); - const apis = [ - { - pathname: '/api/apm/alerts/chart_preview/transaction_error_rate', - params: { transactionType: 'request' }, - }, - { pathname: '/api/apm/alerts/chart_preview/transaction_error_count', params: {} }, - { - pathname: '/api/apm/alerts/chart_preview/transaction_duration', - params: { transactionType: 'request' }, - }, - ]; - - apis.forEach((api) => { - const url = format({ - pathname: api.pathname, + const getOptions = () => ({ + params: { query: { start, end, serviceName: 'opbeans-java', - ...api.params, + transactionType: 'request' as string | undefined, }, + }, + }); + + registry.when(`without data loaded`, { config: 'basic', archives: [] }, () => { + it('transaction_error_rate (without data)', async () => { + const options = getOptions(); + const response = await apmApiSupertest({ + endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_rate', + ...options, + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview).to.eql([]); }); - registry.when( - `GET ${api.pathname} without data loaded`, - { config: 'basic', archives: [] }, - () => { - it('handles the empty state', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect(response.body).to.eql([]); - }); - } - ); - - registry.when( - `GET ${api.pathname} with data loaded`, - { config: 'basic', archives: [archiveName] }, - () => { - it('returns the correct data', async () => { - const response = await supertest.get(url); - - expect(response.status).to.be(200); - expect( - response.body.some((item: { x: number; y: number | null }) => item.x && item.y) - ).to.equal(true); - }); - } - ); + it('transaction_error_count (without data)', async () => { + const options = getOptions(); + options.params.query.transactionType = undefined; + + const response = await apmApiSupertest({ + endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_count', + ...options, + }); + + expect(response.status).to.be(200); + expect(response.body.errorCountChartPreview).to.eql([]); + }); + + it('transaction_duration (without data)', async () => { + const options = getOptions(); + + const response = await apmApiSupertest({ + endpoint: 'GET /api/apm/alerts/chart_preview/transaction_duration', + ...options, + }); + + expect(response.status).to.be(200); + expect(response.body.latencyChartPreview).to.eql([]); + }); + }); + + registry.when(`with data loaded`, { config: 'basic', archives: [archiveName] }, () => { + it('transaction_error_rate (with data)', async () => { + const options = getOptions(); + const response = await apmApiSupertest({ + endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_rate', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.some( + (item: { x: number; y: number | null }) => item.x && item.y + ) + ).to.equal(true); + }); + + it('transaction_error_count (with data)', async () => { + const options = getOptions(); + options.params.query.transactionType = undefined; + + const response = await apmApiSupertest({ + endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_count', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorCountChartPreview.some( + (item: { x: number; y: number | null }) => item.x && item.y + ) + ).to.equal(true); + }); + + it('transaction_duration (with data)', async () => { + const options = getOptions(); + const response = await apmApiSupertest({ + ...options, + endpoint: 'GET /api/apm/alerts/chart_preview/transaction_duration', + }); + + expect(response.status).to.be(200); + expect( + response.body.latencyChartPreview.some( + (item: { x: number; y: number | null }) => item.x && item.y + ) + ).to.equal(true); + }); }); } diff --git a/x-pack/test/apm_api_integration/tests/csm/__snapshots__/page_load_dist.snap b/x-pack/test/apm_api_integration/tests/csm/__snapshots__/page_load_dist.snap index e21069ba2f0a6..9e4a708fae304 100644 --- a/x-pack/test/apm_api_integration/tests/csm/__snapshots__/page_load_dist.snap +++ b/x-pack/test/apm_api_integration/tests/csm/__snapshots__/page_load_dist.snap @@ -2,464 +2,10 @@ exports[`APM API tests trial 8.0.0,rum_8.0.0 UX page load dist with data returns page load distribution 1`] = ` Object { - "maxDuration": 54.46, - "minDuration": 0, - "pageLoadDistribution": Array [ - Object { - "x": 0, - "y": 0, - }, - Object { - "x": 0.5, - "y": 0, - }, - Object { - "x": 1, - "y": 0, - }, - Object { - "x": 1.5, - "y": 0, - }, - Object { - "x": 2, - "y": 0, - }, - Object { - "x": 2.5, - "y": 0, - }, - Object { - "x": 3, - "y": 16.6666666666667, - }, - Object { - "x": 3.5, - "y": 0, - }, - Object { - "x": 4, - "y": 0, - }, - Object { - "x": 4.5, - "y": 0, - }, - Object { - "x": 5, - "y": 50, - }, - Object { - "x": 5.5, - "y": 0, - }, - Object { - "x": 6, - "y": 0, - }, - Object { - "x": 6.5, - "y": 0, - }, - Object { - "x": 7, - "y": 0, - }, - Object { - "x": 7.5, - "y": 0, - }, - Object { - "x": 8, - "y": 0, - }, - Object { - "x": 8.5, - "y": 0, - }, - Object { - "x": 9, - "y": 0, - }, - Object { - "x": 9.5, - "y": 0, - }, - Object { - "x": 10, - "y": 0, - }, - Object { - "x": 10.5, - "y": 0, - }, - Object { - "x": 11, - "y": 0, - }, - Object { - "x": 11.5, - "y": 0, - }, - Object { - "x": 12, - "y": 0, - }, - Object { - "x": 12.5, - "y": 0, - }, - Object { - "x": 13, - "y": 0, - }, - Object { - "x": 13.5, - "y": 0, - }, - Object { - "x": 14, - "y": 0, - }, - Object { - "x": 14.5, - "y": 0, - }, - Object { - "x": 15, - "y": 0, - }, - Object { - "x": 15.5, - "y": 0, - }, - Object { - "x": 16, - "y": 0, - }, - Object { - "x": 16.5, - "y": 0, - }, - Object { - "x": 17, - "y": 0, - }, - Object { - "x": 17.5, - "y": 0, - }, - Object { - "x": 18, - "y": 0, - }, - Object { - "x": 18.5, - "y": 0, - }, - Object { - "x": 19, - "y": 0, - }, - Object { - "x": 19.5, - "y": 0, - }, - Object { - "x": 20, - "y": 0, - }, - Object { - "x": 20.5, - "y": 0, - }, - Object { - "x": 21, - "y": 0, - }, - Object { - "x": 21.5, - "y": 0, - }, - Object { - "x": 22, - "y": 0, - }, - Object { - "x": 22.5, - "y": 0, - }, - Object { - "x": 23, - "y": 0, - }, - Object { - "x": 23.5, - "y": 0, - }, - Object { - "x": 24, - "y": 0, - }, - Object { - "x": 24.5, - "y": 0, - }, - Object { - "x": 25, - "y": 0, - }, - Object { - "x": 25.5, - "y": 0, - }, - Object { - "x": 26, - "y": 0, - }, - Object { - "x": 26.5, - "y": 0, - }, - Object { - "x": 27, - "y": 0, - }, - Object { - "x": 27.5, - "y": 0, - }, - Object { - "x": 28, - "y": 0, - }, - Object { - "x": 28.5, - "y": 0, - }, - Object { - "x": 29, - "y": 0, - }, - Object { - "x": 29.5, - "y": 0, - }, - Object { - "x": 30, - "y": 0, - }, - Object { - "x": 30.5, - "y": 0, - }, - Object { - "x": 31, - "y": 0, - }, - Object { - "x": 31.5, - "y": 0, - }, - Object { - "x": 32, - "y": 0, - }, - Object { - "x": 32.5, - "y": 0, - }, - Object { - "x": 33, - "y": 0, - }, - Object { - "x": 33.5, - "y": 0, - }, - Object { - "x": 34, - "y": 0, - }, - Object { - "x": 34.5, - "y": 0, - }, - Object { - "x": 35, - "y": 0, - }, - Object { - "x": 35.5, - "y": 0, - }, - Object { - "x": 36, - "y": 0, - }, - Object { - "x": 36.5, - "y": 0, - }, - Object { - "x": 37, - "y": 0, - }, - Object { - "x": 37.5, - "y": 16.6666666666667, - }, - Object { - "x": 38, - "y": 0, - }, - Object { - "x": 38.5, - "y": 0, - }, - Object { - "x": 39, - "y": 0, - }, - Object { - "x": 39.5, - "y": 0, - }, - Object { - "x": 40, - "y": 0, - }, - Object { - "x": 40.5, - "y": 0, - }, - Object { - "x": 41, - "y": 0, - }, - Object { - "x": 41.5, - "y": 0, - }, - Object { - "x": 42, - "y": 0, - }, - Object { - "x": 42.5, - "y": 0, - }, - Object { - "x": 43, - "y": 0, - }, - Object { - "x": 43.5, - "y": 0, - }, - Object { - "x": 44, - "y": 0, - }, - Object { - "x": 44.5, - "y": 0, - }, - Object { - "x": 45, - "y": 0, - }, - Object { - "x": 45.5, - "y": 0, - }, - Object { - "x": 46, - "y": 0, - }, - Object { - "x": 46.5, - "y": 0, - }, - Object { - "x": 47, - "y": 0, - }, - Object { - "x": 47.5, - "y": 0, - }, - Object { - "x": 48, - "y": 0, - }, - Object { - "x": 48.5, - "y": 0, - }, - Object { - "x": 49, - "y": 0, - }, - Object { - "x": 49.5, - "y": 0, - }, - Object { - "x": 50, - "y": 0, - }, - Object { - "x": 50.5, - "y": 0, - }, - Object { - "x": 51, - "y": 0, - }, - Object { - "x": 51.5, - "y": 0, - }, - Object { - "x": 52, - "y": 0, - }, - Object { - "x": 52.5, - "y": 0, - }, - Object { - "x": 53, - "y": 0, - }, - Object { - "x": 53.5, - "y": 0, - }, - Object { - "x": 54, - "y": 0, - }, - Object { - "x": 54.5, - "y": 16.6666666666667, - }, - ], - "percentiles": Object { - "50.0": 4.88, - "75.0": 37.09, - "90.0": 37.09, - "95.0": 54.46, - "99.0": 54.46, - }, -} -`; - -exports[`APM API tests trial 8.0.0,rum_8.0.0 UX page load dist with data returns page load distribution with breakdown 1`] = ` -Array [ - Object { - "data": Array [ + "pageLoadDistribution": Object { + "maxDuration": 54.46, + "minDuration": 0, + "pageLoadDistribution": Array [ Object { "x": 0, "y": 0, @@ -486,7 +32,7 @@ Array [ }, Object { "x": 3, - "y": 25, + "y": 16.6666666666667, }, Object { "x": 3.5, @@ -502,7 +48,7 @@ Array [ }, Object { "x": 5, - "y": 25, + "y": 50, }, Object { "x": 5.5, @@ -762,63 +308,525 @@ Array [ }, Object { "x": 37.5, - "y": 25, + "y": 16.6666666666667, }, - ], - "name": "Chrome", - }, - Object { - "data": Array [ Object { - "x": 0, + "x": 38, "y": 0, }, Object { - "x": 0.5, + "x": 38.5, "y": 0, }, Object { - "x": 1, + "x": 39, "y": 0, }, Object { - "x": 1.5, + "x": 39.5, "y": 0, }, Object { - "x": 2, + "x": 40, "y": 0, }, Object { - "x": 2.5, + "x": 40.5, "y": 0, }, Object { - "x": 3, + "x": 41, "y": 0, }, Object { - "x": 3.5, + "x": 41.5, "y": 0, }, Object { - "x": 4, + "x": 42, "y": 0, }, Object { - "x": 4.5, + "x": 42.5, "y": 0, }, Object { - "x": 5, - "y": 100, + "x": 43, + "y": 0, + }, + Object { + "x": 43.5, + "y": 0, + }, + Object { + "x": 44, + "y": 0, + }, + Object { + "x": 44.5, + "y": 0, + }, + Object { + "x": 45, + "y": 0, + }, + Object { + "x": 45.5, + "y": 0, + }, + Object { + "x": 46, + "y": 0, + }, + Object { + "x": 46.5, + "y": 0, + }, + Object { + "x": 47, + "y": 0, + }, + Object { + "x": 47.5, + "y": 0, + }, + Object { + "x": 48, + "y": 0, + }, + Object { + "x": 48.5, + "y": 0, + }, + Object { + "x": 49, + "y": 0, + }, + Object { + "x": 49.5, + "y": 0, + }, + Object { + "x": 50, + "y": 0, + }, + Object { + "x": 50.5, + "y": 0, + }, + Object { + "x": 51, + "y": 0, + }, + Object { + "x": 51.5, + "y": 0, + }, + Object { + "x": 52, + "y": 0, + }, + Object { + "x": 52.5, + "y": 0, + }, + Object { + "x": 53, + "y": 0, + }, + Object { + "x": 53.5, + "y": 0, + }, + Object { + "x": 54, + "y": 0, + }, + Object { + "x": 54.5, + "y": 16.6666666666667, }, ], - "name": "Chrome Mobile", + "percentiles": Object { + "50.0": 4.88, + "75.0": 37.09, + "90.0": 37.09, + "95.0": 54.46, + "99.0": 54.46, + }, }, -] +} `; -exports[`APM API tests trial no data UX page load dist without data returns empty list 1`] = `Object {}`; +exports[`APM API tests trial 8.0.0,rum_8.0.0 UX page load dist with data returns page load distribution with breakdown 1`] = ` +Object { + "pageLoadDistBreakdown": Array [ + Object { + "data": Array [ + Object { + "x": 0, + "y": 0, + }, + Object { + "x": 0.5, + "y": 0, + }, + Object { + "x": 1, + "y": 0, + }, + Object { + "x": 1.5, + "y": 0, + }, + Object { + "x": 2, + "y": 0, + }, + Object { + "x": 2.5, + "y": 0, + }, + Object { + "x": 3, + "y": 25, + }, + Object { + "x": 3.5, + "y": 0, + }, + Object { + "x": 4, + "y": 0, + }, + Object { + "x": 4.5, + "y": 0, + }, + Object { + "x": 5, + "y": 25, + }, + Object { + "x": 5.5, + "y": 0, + }, + Object { + "x": 6, + "y": 0, + }, + Object { + "x": 6.5, + "y": 0, + }, + Object { + "x": 7, + "y": 0, + }, + Object { + "x": 7.5, + "y": 0, + }, + Object { + "x": 8, + "y": 0, + }, + Object { + "x": 8.5, + "y": 0, + }, + Object { + "x": 9, + "y": 0, + }, + Object { + "x": 9.5, + "y": 0, + }, + Object { + "x": 10, + "y": 0, + }, + Object { + "x": 10.5, + "y": 0, + }, + Object { + "x": 11, + "y": 0, + }, + Object { + "x": 11.5, + "y": 0, + }, + Object { + "x": 12, + "y": 0, + }, + Object { + "x": 12.5, + "y": 0, + }, + Object { + "x": 13, + "y": 0, + }, + Object { + "x": 13.5, + "y": 0, + }, + Object { + "x": 14, + "y": 0, + }, + Object { + "x": 14.5, + "y": 0, + }, + Object { + "x": 15, + "y": 0, + }, + Object { + "x": 15.5, + "y": 0, + }, + Object { + "x": 16, + "y": 0, + }, + Object { + "x": 16.5, + "y": 0, + }, + Object { + "x": 17, + "y": 0, + }, + Object { + "x": 17.5, + "y": 0, + }, + Object { + "x": 18, + "y": 0, + }, + Object { + "x": 18.5, + "y": 0, + }, + Object { + "x": 19, + "y": 0, + }, + Object { + "x": 19.5, + "y": 0, + }, + Object { + "x": 20, + "y": 0, + }, + Object { + "x": 20.5, + "y": 0, + }, + Object { + "x": 21, + "y": 0, + }, + Object { + "x": 21.5, + "y": 0, + }, + Object { + "x": 22, + "y": 0, + }, + Object { + "x": 22.5, + "y": 0, + }, + Object { + "x": 23, + "y": 0, + }, + Object { + "x": 23.5, + "y": 0, + }, + Object { + "x": 24, + "y": 0, + }, + Object { + "x": 24.5, + "y": 0, + }, + Object { + "x": 25, + "y": 0, + }, + Object { + "x": 25.5, + "y": 0, + }, + Object { + "x": 26, + "y": 0, + }, + Object { + "x": 26.5, + "y": 0, + }, + Object { + "x": 27, + "y": 0, + }, + Object { + "x": 27.5, + "y": 0, + }, + Object { + "x": 28, + "y": 0, + }, + Object { + "x": 28.5, + "y": 0, + }, + Object { + "x": 29, + "y": 0, + }, + Object { + "x": 29.5, + "y": 0, + }, + Object { + "x": 30, + "y": 0, + }, + Object { + "x": 30.5, + "y": 0, + }, + Object { + "x": 31, + "y": 0, + }, + Object { + "x": 31.5, + "y": 0, + }, + Object { + "x": 32, + "y": 0, + }, + Object { + "x": 32.5, + "y": 0, + }, + Object { + "x": 33, + "y": 0, + }, + Object { + "x": 33.5, + "y": 0, + }, + Object { + "x": 34, + "y": 0, + }, + Object { + "x": 34.5, + "y": 0, + }, + Object { + "x": 35, + "y": 0, + }, + Object { + "x": 35.5, + "y": 0, + }, + Object { + "x": 36, + "y": 0, + }, + Object { + "x": 36.5, + "y": 0, + }, + Object { + "x": 37, + "y": 0, + }, + Object { + "x": 37.5, + "y": 25, + }, + ], + "name": "Chrome", + }, + Object { + "data": Array [ + Object { + "x": 0, + "y": 0, + }, + Object { + "x": 0.5, + "y": 0, + }, + Object { + "x": 1, + "y": 0, + }, + Object { + "x": 1.5, + "y": 0, + }, + Object { + "x": 2, + "y": 0, + }, + Object { + "x": 2.5, + "y": 0, + }, + Object { + "x": 3, + "y": 0, + }, + Object { + "x": 3.5, + "y": 0, + }, + Object { + "x": 4, + "y": 0, + }, + Object { + "x": 4.5, + "y": 0, + }, + Object { + "x": 5, + "y": 100, + }, + ], + "name": "Chrome Mobile", + }, + ], +} +`; + +exports[`APM API tests trial no data UX page load dist without data returns empty list 1`] = ` +Object { + "pageLoadDistribution": null, +} +`; exports[`APM API tests trial no data UX page load dist without data returns empty list with breakdowns 1`] = `Object {}`; diff --git a/x-pack/test/apm_api_integration/tests/csm/csm_services.ts b/x-pack/test/apm_api_integration/tests/csm/csm_services.ts index c0f92f5f2accf..57018b5012aa2 100644 --- a/x-pack/test/apm_api_integration/tests/csm/csm_services.ts +++ b/x-pack/test/apm_api_integration/tests/csm/csm_services.ts @@ -19,7 +19,7 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) ); expect(response.status).to.be(200); - expect(response.body).to.eql([]); + expect(response.body.rumServices).to.eql([]); }); }); @@ -34,7 +34,7 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) expect(response.status).to.be(200); - expectSnapshot(response.body).toMatchInline(`Array []`); + expectSnapshot(response.body.rumServices).toMatchInline(`Array []`); }); } ); diff --git a/x-pack/test/apm_api_integration/tests/feature_controls.ts b/x-pack/test/apm_api_integration/tests/feature_controls.ts index edeffe1e5c296..553f22fc2279e 100644 --- a/x-pack/test/apm_api_integration/tests/feature_controls.ts +++ b/x-pack/test/apm_api_integration/tests/feature_controls.ts @@ -42,9 +42,9 @@ export default function featureControlsTests({ getService }: FtrProviderContext) } const endpoints: Endpoint[] = [ { - // this doubles as a smoke test for the _debug query parameter + // this doubles as a smoke test for the _inspect query parameter req: { - url: `/api/apm/services/foo/errors?start=${start}&end=${end}&_debug=true`, + url: `/api/apm/services/foo/errors?start=${start}&end=${end}&_inspect=true`, }, expectForbidden: expect403, expectResponse: expect200, diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 49a568e0051a7..9f0f1b15c0580 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -13,61 +13,187 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte describe('APM API tests', function () { this.tags('ciGroup1'); - loadTestFile(require.resolve('./alerts/chart_preview')); - loadTestFile(require.resolve('./correlations/slow_transactions')); + // inspect feature + describe('inspect/inspect', function () { + loadTestFile(require.resolve('./inspect/inspect')); + }); - loadTestFile(require.resolve('./csm/csm_services')); - loadTestFile(require.resolve('./csm/has_rum_data')); - loadTestFile(require.resolve('./csm/js_errors')); - loadTestFile(require.resolve('./csm/long_task_metrics')); - loadTestFile(require.resolve('./csm/page_load_dist')); - loadTestFile(require.resolve('./csm/page_views')); - loadTestFile(require.resolve('./csm/url_search')); - loadTestFile(require.resolve('./csm/web_core_vitals')); + // alerts + describe('alerts/chart_preview', function () { + loadTestFile(require.resolve('./alerts/chart_preview')); + }); - loadTestFile(require.resolve('./metrics_charts/metrics_charts')); - - loadTestFile(require.resolve('./observability_overview/has_data')); - loadTestFile(require.resolve('./observability_overview/observability_overview')); - - loadTestFile(require.resolve('./service_maps/service_maps')); - - loadTestFile(require.resolve('./service_overview/dependencies')); - loadTestFile(require.resolve('./service_overview/instances_primary_statistics')); - loadTestFile(require.resolve('./service_overview/instances_comparison_statistics')); - - loadTestFile(require.resolve('./services/agent_name')); - loadTestFile(require.resolve('./services/annotations')); - loadTestFile(require.resolve('./services/service_details')); - loadTestFile(require.resolve('./services/service_icons')); - loadTestFile(require.resolve('./services/throughput')); - loadTestFile(require.resolve('./services/top_services')); - loadTestFile(require.resolve('./services/transaction_types')); - loadTestFile(require.resolve('./services/error_groups_primary_statistics')); - loadTestFile(require.resolve('./services/error_groups_comparison_statistics')); - - loadTestFile(require.resolve('./settings/anomaly_detection/basic')); - loadTestFile(require.resolve('./settings/anomaly_detection/no_access_user')); - loadTestFile(require.resolve('./settings/anomaly_detection/read_user')); - loadTestFile(require.resolve('./settings/anomaly_detection/write_user')); - - loadTestFile(require.resolve('./settings/agent_configuration')); - - loadTestFile(require.resolve('./settings/custom_link')); - - loadTestFile(require.resolve('./traces/top_traces')); - - loadTestFile(require.resolve('./transactions/breakdown')); - loadTestFile(require.resolve('./transactions/distribution')); - loadTestFile(require.resolve('./transactions/error_rate')); - loadTestFile(require.resolve('./transactions/latency')); - loadTestFile(require.resolve('./transactions/throughput')); - loadTestFile(require.resolve('./transactions/top_transaction_groups')); - loadTestFile(require.resolve('./transactions/transactions_groups_primary_statistics')); - loadTestFile(require.resolve('./transactions/transactions_groups_comparison_statistics')); - - loadTestFile(require.resolve('./feature_controls')); + describe('correlations/slow_transactions', function () { + loadTestFile(require.resolve('./correlations/slow_transactions')); + }); + + describe('metrics_charts/metrics_charts', function () { + loadTestFile(require.resolve('./metrics_charts/metrics_charts')); + }); + + describe('observability_overview/has_data', function () { + loadTestFile(require.resolve('./observability_overview/has_data')); + }); + + describe('observability_overview/observability_overview', function () { + loadTestFile(require.resolve('./observability_overview/observability_overview')); + }); + + describe('service_maps/service_maps', function () { + loadTestFile(require.resolve('./service_maps/service_maps')); + }); + + // Service overview + describe('service_overview/dependencies', function () { + loadTestFile(require.resolve('./service_overview/dependencies')); + }); + + describe('service_overview/instances_primary_statistics', function () { + loadTestFile(require.resolve('./service_overview/instances_primary_statistics')); + }); + + describe('service_overview/instances_comparison_statistics', function () { + loadTestFile(require.resolve('./service_overview/instances_comparison_statistics')); + }); + + // Services + describe('services/agent_name', function () { + loadTestFile(require.resolve('./services/agent_name')); + }); + + describe('services/annotations', function () { + loadTestFile(require.resolve('./services/annotations')); + }); + + describe('services/service_details', function () { + loadTestFile(require.resolve('./services/service_details')); + }); + + describe('services/service_icons', function () { + loadTestFile(require.resolve('./services/service_icons')); + }); + + describe('services/throughput', function () { + loadTestFile(require.resolve('./services/throughput')); + }); + + describe('services/top_services', function () { + loadTestFile(require.resolve('./services/top_services')); + }); + + describe('services/transaction_types', function () { + loadTestFile(require.resolve('./services/transaction_types')); + }); + + describe('services/error_groups_primary_statistics', function () { + loadTestFile(require.resolve('./services/error_groups_primary_statistics')); + }); + + describe('services/error_groups_comparison_statistics', function () { + loadTestFile(require.resolve('./services/error_groups_comparison_statistics')); + }); + + // Settinges + describe('settings/anomaly_detection/basic', function () { + loadTestFile(require.resolve('./settings/anomaly_detection/basic')); + }); + + describe('settings/anomaly_detection/no_access_user', function () { + loadTestFile(require.resolve('./settings/anomaly_detection/no_access_user')); + }); + + describe('settings/anomaly_detection/read_user', function () { + loadTestFile(require.resolve('./settings/anomaly_detection/read_user')); + }); + + describe('settings/anomaly_detection/write_user', function () { + loadTestFile(require.resolve('./settings/anomaly_detection/write_user')); + }); + + describe('settings/agent_configuration', function () { + loadTestFile(require.resolve('./settings/agent_configuration')); + }); + + describe('settings/custom_link', function () { + loadTestFile(require.resolve('./settings/custom_link')); + }); + + // traces + describe('traces/top_traces', function () { + loadTestFile(require.resolve('./traces/top_traces')); + }); + + // transactions + describe('transactions/breakdown', function () { + loadTestFile(require.resolve('./transactions/breakdown')); + }); + + describe('transactions/distribution', function () { + loadTestFile(require.resolve('./transactions/distribution')); + }); + + describe('transactions/error_rate', function () { + loadTestFile(require.resolve('./transactions/error_rate')); + }); + + describe('transactions/latency', function () { + loadTestFile(require.resolve('./transactions/latency')); + }); + + describe('transactions/throughput', function () { + loadTestFile(require.resolve('./transactions/throughput')); + }); + + describe('transactions/top_transaction_groups', function () { + loadTestFile(require.resolve('./transactions/top_transaction_groups')); + }); + + describe('transactions/transactions_groups_primary_statistics', function () { + loadTestFile(require.resolve('./transactions/transactions_groups_primary_statistics')); + }); + + describe('transactions/transactions_groups_comparison_statistics', function () { + loadTestFile(require.resolve('./transactions/transactions_groups_comparison_statistics')); + }); + + // feature control + describe('feature_controls', function () { + loadTestFile(require.resolve('./feature_controls')); + }); + + // CSM + describe('csm/csm_services', function () { + loadTestFile(require.resolve('./csm/csm_services')); + }); + + describe('csm/has_rum_data', function () { + loadTestFile(require.resolve('./csm/has_rum_data')); + }); + + describe('csm/js_errors', function () { + loadTestFile(require.resolve('./csm/js_errors')); + }); + + describe('csm/long_task_metrics', function () { + loadTestFile(require.resolve('./csm/long_task_metrics')); + }); + + describe('csm/page_load_dist', function () { + loadTestFile(require.resolve('./csm/page_load_dist')); + }); + + describe('csm/page_views', function () { + loadTestFile(require.resolve('./csm/page_views')); + }); + + describe('csm/url_search', function () { + loadTestFile(require.resolve('./csm/url_search')); + }); + + describe('csm/web_core_vitals', function () { + loadTestFile(require.resolve('./csm/web_core_vitals')); + }); registry.run(providerContext); }); diff --git a/x-pack/test/apm_api_integration/tests/inspect/inspect.ts b/x-pack/test/apm_api_integration/tests/inspect/inspect.ts new file mode 100644 index 0000000000000..aae2e38e8ec8e --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/inspect/inspect.ts @@ -0,0 +1,98 @@ +/* + * 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 { createApmApiSupertest } from '../../common/apm_api_supertest'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; + +export default function customLinksTests({ getService }: FtrProviderContext) { + const supertestRead = createApmApiSupertest(getService('supertest')); + + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + + registry.when('Inspect feature', { config: 'trial', archives: [archiveName] }, () => { + describe('when omitting `_inspect` query param', () => { + it('returns response without `_inspect`', async () => { + const { status, body } = await supertestRead({ + endpoint: 'GET /api/apm/environments', + params: { + query: { + start: metadata.start, + end: metadata.end, + }, + }, + }); + + expect(status).to.be(200); + expect(body._inspect).to.be(undefined); + }); + }); + + describe('when passing `_inspect` as query param', () => { + describe('elasticsearch calls made with end-user auth are returned', () => { + it('for environments', async () => { + const { status, body } = await supertestRead({ + endpoint: 'GET /api/apm/environments', + params: { + query: { + start: metadata.start, + end: metadata.end, + _inspect: true, + }, + }, + }); + expect(status).to.be(200); + expect(body._inspect?.length).to.be(1); + + // @ts-expect-error + expect(Object.keys(body._inspect[0])).to.eql([ + 'response', + 'duration', + 'requestType', + 'requestParams', + ]); + }); + }); + + describe('elasticsearch calls made with internal user are not return', () => { + it('for custom links', async () => { + const { status, body } = await supertestRead({ + endpoint: 'GET /api/apm/settings/custom_links', + params: { + query: { + 'service.name': 'opbeans-node', + 'transaction.type': 'request', + _inspect: true, + }, + }, + }); + + expect(status).to.be(200); + expect(body._inspect).to.eql([]); + }); + + it('for agent configs', async () => { + const { status, body } = await supertestRead({ + endpoint: 'GET /api/apm/settings/agent-configuration', + // @ts-expect-error + params: { + query: { + _inspect: true, + }, + }, + }); + + expect(status).to.be(200); + expect(body._inspect).to.eql([]); + }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/observability_overview/has_data.ts b/x-pack/test/apm_api_integration/tests/observability_overview/has_data.ts index a86063bcab58a..c6bdce217e229 100644 --- a/x-pack/test/apm_api_integration/tests/observability_overview/has_data.ts +++ b/x-pack/test/apm_api_integration/tests/observability_overview/has_data.ts @@ -6,20 +6,24 @@ */ import expect from '@kbn/expect'; +import { createApmApiSupertest } from '../../common/apm_api_supertest'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const apmApiSupertest = createApmApiSupertest(supertest); registry.when( 'Observability overview when data is not loaded', { config: 'basic', archives: [] }, () => { it('returns false when there is no data', async () => { - const response = await supertest.get('/api/apm/observability_overview/has_data'); + const response = await apmApiSupertest({ + endpoint: 'GET /api/apm/observability_overview/has_data', + }); expect(response.status).to.be(200); - expect(response.body).to.eql(false); + expect(response.body.hasData).to.eql(false); }); } ); @@ -29,9 +33,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: ['observability_overview'] }, () => { it('returns false when there is only onboarding data', async () => { - const response = await supertest.get('/api/apm/observability_overview/has_data'); + const response = await apmApiSupertest({ + endpoint: 'GET /api/apm/observability_overview/has_data', + }); expect(response.status).to.be(200); - expect(response.body).to.eql(false); + expect(response.body.hasData).to.eql(false); }); } ); @@ -41,9 +47,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: ['apm_8.0.0'] }, () => { it('returns true when there is at least one document on transaction, error or metrics indices', async () => { - const response = await supertest.get('/api/apm/observability_overview/has_data'); + const response = await apmApiSupertest({ + endpoint: 'GET /api/apm/observability_overview/has_data', + }); expect(response.status).to.be(200); - expect(response.body).to.eql(true); + expect(response.body.hasData).to.eql(true); }); } ); diff --git a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts index fde1210551816..142802840974d 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts @@ -7,8 +7,8 @@ import expect from '@kbn/expect'; import { last, omit, pick, sortBy } from 'lodash'; -import url from 'url'; import { ValuesType } from 'utility-types'; +import { createApmApiSupertest } from '../../../common/apm_api_supertest'; import { roundNumber } from '../../../utils'; import { ENVIRONMENT_ALL } from '../../../../../plugins/apm/common/environment_filter_values'; import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi'; @@ -18,7 +18,7 @@ import { registry } from '../../../common/registry'; import { apmDependenciesMapping, createServiceDependencyDocs } from './es_utils'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const apmApiSupertest = createApmApiSupertest(getService('supertest')); const es = getService('es'); const archiveName = 'apm_8.0.0'; @@ -29,20 +29,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: [] }, () => { it('handles the empty state', async () => { - const response = await supertest.get( - url.format({ - pathname: `/api/apm/services/opbeans-java/dependencies`, + const response = await apmApiSupertest({ + endpoint: `GET /api/apm/services/{serviceName}/dependencies`, + params: { + path: { serviceName: 'opbeans-java' }, query: { start, end, numBuckets: 20, environment: ENVIRONMENT_ALL.value, }, - }) - ); + }, + }); expect(response.status).to.be(200); - expect(response.body).to.eql([]); + expect(response.body.serviceDependencies).to.eql([]); }); } ); @@ -203,17 +204,18 @@ export default function ApiTest({ getService }: FtrProviderContext) { refresh: 'wait_for', }); - response = await supertest.get( - url.format({ - pathname: `/api/apm/services/opbeans-java/dependencies`, + response = await apmApiSupertest({ + endpoint: `GET /api/apm/services/{serviceName}/dependencies`, + params: { + path: { serviceName: 'opbeans-java' }, query: { start, end, numBuckets: 20, environment: ENVIRONMENT_ALL.value, }, - }) - ); + }, + }); }); it('returns a 200', () => { @@ -221,11 +223,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns two dependencies', () => { - expect(response.body.length).to.be(2); + expect(response.body.serviceDependencies.length).to.be(2); }); it('returns opbeans-node as a dependency', () => { - const opbeansNode = response.body.find( + const opbeansNode = response.body.serviceDependencies.find( (item) => item.type === 'service' && item.serviceName === 'opbeans-node' ); @@ -261,7 +263,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns postgres as an external dependency', () => { - const postgres = response.body.find( + const postgres = response.body.serviceDependencies.find( (item) => item.type === 'external' && item.name === 'postgres' ); @@ -302,17 +304,18 @@ export default function ApiTest({ getService }: FtrProviderContext) { }; before(async () => { - response = await supertest.get( - url.format({ - pathname: `/api/apm/services/opbeans-python/dependencies`, + response = await apmApiSupertest({ + endpoint: `GET /api/apm/services/{serviceName}/dependencies`, + params: { + path: { serviceName: 'opbeans-python' }, query: { start, end, numBuckets: 20, environment: ENVIRONMENT_ALL.value, }, - }) - ); + }, + }); }); it('returns a successful response', () => { @@ -320,10 +323,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns at least one item', () => { - expect(response.body.length).to.be.greaterThan(0); + expect(response.body.serviceDependencies.length).to.be.greaterThan(0); expectSnapshot( - omit(response.body[0], [ + omit(response.body.serviceDependencies[0], [ 'errorRate.timeseries', 'throughput.timeseries', 'latency.timeseries', @@ -349,7 +352,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns the right names', () => { - const names = response.body.map((item) => item.name); + const names = response.body.serviceDependencies.map((item) => item.name); expectSnapshot(names.sort()).toMatchInline(` Array [ "elasticsearch", @@ -361,7 +364,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns the right service names', () => { - const serviceNames = response.body + const serviceNames = response.body.serviceDependencies .map((item) => (item.type === 'service' ? item.serviceName : undefined)) .filter(Boolean); @@ -374,7 +377,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the right latency values', () => { const latencyValues = sortBy( - response.body.map((item) => ({ name: item.name, latency: item.latency.value })), + response.body.serviceDependencies.map((item) => ({ + name: item.name, + latency: item.latency.value, + })), 'name' ); @@ -402,7 +408,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the right throughput values', () => { const throughputValues = sortBy( - response.body.map((item) => ({ name: item.name, throughput: item.throughput.value })), + response.body.serviceDependencies.map((item) => ({ + name: item.name, + throughput: item.throughput.value, + })), 'name' ); @@ -430,7 +439,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the right impact values', () => { const impactValues = sortBy( - response.body.map((item) => ({ + response.body.serviceDependencies.map((item) => ({ name: item.name, impact: item.impact, latency: item.latency.value, diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_primary_statistics.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_primary_statistics.ts index 9cf11068f0a2f..aac92685a3c34 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances_primary_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instances_primary_statistics.ts @@ -6,45 +6,41 @@ */ import expect from '@kbn/expect'; -import url from 'url'; import { pick, sortBy } from 'lodash'; -import { isFiniteNumber } from '../../../../plugins/apm/common/utils/is_finite_number'; import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; +import { isFiniteNumber } from '../../../../plugins/apm/common/utils/is_finite_number'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { registry } from '../../common/registry'; +import { createApmApiSupertest } from '../../common/apm_api_supertest'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const apmApiSupertest = createApmApiSupertest(getService('supertest')); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; - interface Response { - status: number; - body: APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/primary_statistics'>; - } - registry.when( 'Service overview instances primary statistics when data is not loaded', { config: 'basic', archives: [] }, () => { describe('when data is not loaded', () => { it('handles the empty state', async () => { - const response: Response = await supertest.get( - url.format({ - pathname: `/api/apm/services/opbeans-java/service_overview_instances/primary_statistics`, + const response = await apmApiSupertest({ + endpoint: `GET /api/apm/services/{serviceName}/service_overview_instances/primary_statistics`, + params: { + path: { serviceName: 'opbeans-java' }, query: { latencyAggregationType: 'avg', start, end, transactionType: 'request', }, - }) - ); + }, + }); expect(response.status).to.be(200); - expect(response.body).to.eql([]); + expect(response.body.serviceInstances).to.eql([]); }); }); } @@ -55,28 +51,31 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: [archiveName] }, () => { describe('fetching java data', () => { - let response: Response; + let response: { + body: APIReturnType<`GET /api/apm/services/{serviceName}/service_overview_instances/primary_statistics`>; + }; beforeEach(async () => { - response = await supertest.get( - url.format({ - pathname: `/api/apm/services/opbeans-java/service_overview_instances/primary_statistics`, + response = await apmApiSupertest({ + endpoint: `GET /api/apm/services/{serviceName}/service_overview_instances/primary_statistics`, + params: { + path: { serviceName: 'opbeans-java' }, query: { latencyAggregationType: 'avg', start, end, transactionType: 'request', }, - }) - ); + }, + }); }); it('returns a service node item', () => { - expect(response.body.length).to.be.greaterThan(0); + expect(response.body.serviceInstances.length).to.be.greaterThan(0); }); it('returns statistics for each service node', () => { - const item = response.body[0]; + const item = response.body.serviceInstances[0]; expect(isFiniteNumber(item.cpuUsage)).to.be(true); expect(isFiniteNumber(item.memoryUsage)).to.be(true); @@ -86,7 +85,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns the right data', () => { - const items = sortBy(response.body, 'serviceNodeName'); + const items = sortBy(response.body.serviceInstances, 'serviceNodeName'); const serviceNodeNames = items.map((item) => item.serviceNodeName); @@ -121,24 +120,27 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('fetching non-java data', () => { - let response: Response; + let response: { + body: APIReturnType<`GET /api/apm/services/{serviceName}/service_overview_instances/primary_statistics`>; + }; beforeEach(async () => { - response = await supertest.get( - url.format({ - pathname: `/api/apm/services/opbeans-ruby/service_overview_instances/primary_statistics`, + response = await apmApiSupertest({ + endpoint: `GET /api/apm/services/{serviceName}/service_overview_instances/primary_statistics`, + params: { + path: { serviceName: 'opbeans-ruby' }, query: { latencyAggregationType: 'avg', start, end, transactionType: 'request', }, - }) - ); + }, + }); }); it('returns statistics for each service node', () => { - const item = response.body[0]; + const item = response.body.serviceInstances[0]; expect(isFiniteNumber(item.cpuUsage)).to.be(true); expect(isFiniteNumber(item.memoryUsage)).to.be(true); @@ -148,7 +150,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns the right data', () => { - const items = sortBy(response.body, 'serviceNodeName'); + const items = sortBy(response.body.serviceInstances, 'serviceNodeName'); const serviceNodeNames = items.map((item) => item.serviceNodeName); diff --git a/x-pack/test/apm_api_integration/tests/services/service_icons.ts b/x-pack/test/apm_api_integration/tests/services/service_icons.ts index 2c7313e4d01ef..94188b6321775 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_icons.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_icons.ts @@ -46,11 +46,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); expectSnapshot(response.body).toMatchInline(` - Object { - "agentName": "java", - "containerType": "Kubernetes", - } - `); + Object { + "agentName": "java", + "containerType": "Kubernetes", + } + `); }); it('returns python service icons', async () => { @@ -64,12 +64,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); expectSnapshot(response.body).toMatchInline(` - Object { - "agentName": "python", - "cloudProvider": "gcp", - "containerType": "Kubernetes", - } - `); + Object { + "agentName": "python", + "cloudProvider": "gcp", + "containerType": "Kubernetes", + } + `); }); } ); diff --git a/x-pack/test/apm_api_integration/tests/settings/agent_configuration.ts b/x-pack/test/apm_api_integration/tests/settings/agent_configuration.ts index 14a5cf66c4090..fbd60c0f1ab1a 100644 --- a/x-pack/test/apm_api_integration/tests/settings/agent_configuration.ts +++ b/x-pack/test/apm_api_integration/tests/settings/agent_configuration.ts @@ -9,104 +9,77 @@ import expect from '@kbn/expect'; import { omit, orderBy } from 'lodash'; import { AgentConfigurationIntake } from '../../../../plugins/apm/common/agent_configuration/configuration_types'; import { AgentConfigSearchParams } from '../../../../plugins/apm/server/routes/settings/agent_configuration'; +import { createApmApiSupertest } from '../../common/apm_api_supertest'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function agentConfigurationTests({ getService }: FtrProviderContext) { - const supertestRead = getService('supertestAsApmReadUser'); - const supertestWrite = getService('supertestAsApmWriteUser'); + const supertestRead = createApmApiSupertest(getService('supertestAsApmReadUser')); + const supertestWrite = createApmApiSupertest(getService('supertestAsApmWriteUser')); + const log = getService('log'); const archiveName = 'apm_8.0.0'; function getServices() { - return supertestRead - .get(`/api/apm/settings/agent-configuration/services`) - .set('kbn-xsrf', 'foo'); + return supertestRead({ + endpoint: 'GET /api/apm/settings/agent-configuration/services', + }); } - function getEnvironments(serviceName: string) { - return supertestRead - .get(`/api/apm/settings/agent-configuration/environments?serviceName=${serviceName}`) - .set('kbn-xsrf', 'foo'); + async function getEnvironments(serviceName: string) { + return supertestRead({ + endpoint: 'GET /api/apm/settings/agent-configuration/environments', + params: { query: { serviceName } }, + }); } function getAgentName(serviceName: string) { - return supertestRead - .get(`/api/apm/settings/agent-configuration/agent_name?serviceName=${serviceName}`) - .set('kbn-xsrf', 'foo'); + return supertestRead({ + endpoint: 'GET /api/apm/settings/agent-configuration/agent_name', + params: { query: { serviceName } }, + }); } function searchConfigurations(configuration: AgentConfigSearchParams) { - return supertestRead - .post(`/api/apm/settings/agent-configuration/search`) - .send(configuration) - .set('kbn-xsrf', 'foo'); + return supertestRead({ + endpoint: 'POST /api/apm/settings/agent-configuration/search', + params: { body: configuration }, + }); } function getAllConfigurations() { - return supertestRead.get(`/api/apm/settings/agent-configuration`).set('kbn-xsrf', 'foo'); + return supertestRead({ endpoint: 'GET /api/apm/settings/agent-configuration' }); } - async function createConfiguration(config: AgentConfigurationIntake, { user = 'write' } = {}) { - log.debug('creating configuration', config.service); + function createConfiguration(configuration: AgentConfigurationIntake, { user = 'write' } = {}) { + log.debug('creating configuration', configuration.service); const supertestClient = user === 'read' ? supertestRead : supertestWrite; - const res = await supertestClient - .put(`/api/apm/settings/agent-configuration`) - .send(config) - .set('kbn-xsrf', 'foo'); - - throwOnError(res); - - return res; + return supertestClient({ + endpoint: 'PUT /api/apm/settings/agent-configuration', + params: { body: configuration }, + }); } - async function updateConfiguration(config: AgentConfigurationIntake, { user = 'write' } = {}) { + function updateConfiguration(config: AgentConfigurationIntake, { user = 'write' } = {}) { log.debug('updating configuration', config.service); const supertestClient = user === 'read' ? supertestRead : supertestWrite; - const res = await supertestClient - .put(`/api/apm/settings/agent-configuration?overwrite=true`) - .send(config) - .set('kbn-xsrf', 'foo'); - - throwOnError(res); - - return res; + return supertestClient({ + endpoint: 'PUT /api/apm/settings/agent-configuration', + params: { query: { overwrite: true }, body: config }, + }); } - async function deleteConfiguration( - { service }: AgentConfigurationIntake, - { user = 'write' } = {} - ) { + function deleteConfiguration({ service }: AgentConfigurationIntake, { user = 'write' } = {}) { log.debug('deleting configuration', service); const supertestClient = user === 'read' ? supertestRead : supertestWrite; - const res = await supertestClient - .delete(`/api/apm/settings/agent-configuration`) - .send({ service }) - .set('kbn-xsrf', 'foo'); - - throwOnError(res); - - return res; - } - - function throwOnError(res: any) { - const { statusCode, req, body } = res; - if (statusCode !== 200) { - const e = new Error(` - Endpoint: ${req.method} ${req.path} - Service: ${JSON.stringify(res.request._data.service)} - Status code: ${statusCode} - Response: ${body.message}`); - - // @ts-ignore - e.res = res; - - throw e; - } + return supertestClient({ + endpoint: 'DELETE /api/apm/settings/agent-configuration', + params: { body: { service } }, + }); } registry.when( @@ -115,17 +88,17 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte () => { it('handles the empty state for services', async () => { const { body } = await getServices(); - expect(body).to.eql(['ALL_OPTION_VALUE']); + expect(body.serviceNames).to.eql(['ALL_OPTION_VALUE']); }); it('handles the empty state for environments', async () => { const { body } = await getEnvironments('myservice'); - expect(body).to.eql([{ name: 'ALL_OPTION_VALUE', alreadyConfigured: false }]); + expect(body.environments).to.eql([{ name: 'ALL_OPTION_VALUE', alreadyConfigured: false }]); }); - it('handles the empty state for agent names', async () => { + it('handles the empty state for agent name', async () => { const { body } = await getAgentName('myservice'); - expect(body).to.eql({}); + expect(body.agentName).to.eql(undefined); }); describe('as a read-only user', () => { @@ -160,7 +133,7 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte try { await deleteConfiguration(newConfig, { user: 'read' }); - // ensure that `deleteConfiguration` throws + // ensure that line above throws expect(true).to.be(false); } catch (e) { expect(e.res.statusCode).to.be(403); @@ -182,18 +155,19 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte it('can create and delete config', async () => { // assert that config does not exist - const res1 = await searchConfigurations(searchParams); - expect(res1.status).to.equal(404); + await expectStatusCode(() => searchConfigurations(searchParams), 404); - // assert that config was created + // create config await createConfiguration(newConfig); - const res2 = await searchConfigurations(searchParams); - expect(res2.status).to.equal(200); - // assert that config was deleted + // assert that config now exists + await expectStatusCode(() => searchConfigurations(searchParams), 200); + + // delete config await deleteConfiguration(newConfig); - const res3 = await searchConfigurations(searchParams); - expect(res3.status).to.equal(404); + + // assert that config was deleted + await expectStatusCode(() => searchConfigurations(searchParams), 404); }); describe('when a configuration exists', () => { @@ -209,8 +183,9 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte it('can list the config', async () => { const { status, body } = await getAllConfigurations(); + expect(status).to.equal(200); - expect(omitTimestamp(body)).to.eql([ + expect(omitTimestamp(body.configurations)).to.eql([ { service: {}, settings: { transaction_sample_rate: '0.55' }, @@ -295,7 +270,9 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte it('can list all configs', async () => { const { status, body } = await getAllConfigurations(); expect(status).to.equal(200); - expect(orderBy(omitTimestamp(body), ['settings.transaction_sample_rate'])).to.eql([ + expect( + orderBy(omitTimestamp(body.configurations), ['settings.transaction_sample_rate']) + ).to.eql([ { service: {}, settings: { transaction_sample_rate: '0.1' }, @@ -351,7 +328,7 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte service: { name: 'myservice', environment: 'production' }, settings: { transaction_sample_rate: '0.9' }, }; - let etag: string; + let etag: string | undefined; before(async () => { log.debug('creating agent configuration'); @@ -391,7 +368,7 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte service: { name: 'myservice', environment: 'development' }, }); - return body._source.applied_by_agent; + return !!body._source.applied_by_agent; } // wait until `applied_by_agent` has been updated in elasticsearch @@ -415,7 +392,7 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte service: { name: 'myservice', environment: 'production' }, }); - return body._source.applied_by_agent; + return !!body._source.applied_by_agent; } // wait until `applied_by_agent` has been updated in elasticsearch @@ -432,47 +409,56 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte it('returns all services', async () => { const { body } = await getServices(); expectSnapshot(body).toMatchInline(` - Array [ - "ALL_OPTION_VALUE", - "kibana", - "kibana-frontend", - "opbeans-dotnet", - "opbeans-go", - "opbeans-java", - "opbeans-node", - "opbeans-python", - "opbeans-ruby", - "opbeans-rum", - ] + Object { + "serviceNames": Array [ + "ALL_OPTION_VALUE", + "kibana", + "kibana-frontend", + "opbeans-dotnet", + "opbeans-go", + "opbeans-java", + "opbeans-node", + "opbeans-python", + "opbeans-ruby", + "opbeans-rum", + ], + } `); }); it('returns the environments, all unconfigured', async () => { const { body } = await getEnvironments('opbeans-node'); + const { environments } = body; - expect(body.map((item: { name: string }) => item.name)).to.contain('ALL_OPTION_VALUE'); + expect(environments.map((item: { name: string }) => item.name)).to.contain( + 'ALL_OPTION_VALUE' + ); expect( - body.every((item: { alreadyConfigured: boolean }) => item.alreadyConfigured === false) + environments.every( + (item: { alreadyConfigured: boolean }) => item.alreadyConfigured === false + ) ).to.be(true); expectSnapshot(body).toMatchInline(` - Array [ - Object { - "alreadyConfigured": false, - "name": "ALL_OPTION_VALUE", - }, - Object { - "alreadyConfigured": false, - "name": "testing", - }, - ] + Object { + "environments": Array [ + Object { + "alreadyConfigured": false, + "name": "ALL_OPTION_VALUE", + }, + Object { + "alreadyConfigured": false, + "name": "testing", + }, + ], + } `); }); - it('returns the agent names', async () => { + it('returns the agent name', async () => { const { body } = await getAgentName('opbeans-node'); - expect(body).to.eql({ agentName: 'nodejs' }); + expect(body.agentName).to.eql('nodejs'); }); } ); @@ -494,3 +480,17 @@ async function waitFor(cb: () => Promise, retries = 50): Promise omit(config, '@timestamp')); } + +async function expectStatusCode( + fn: () => Promise<{ + status: number; + }>, + statusCode: number +) { + try { + const res = await fn(); + expect(res.status).to.be(statusCode); + } catch (e) { + expect(e.res.status).to.be(statusCode); + } +} diff --git a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/write_user.ts b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/write_user.ts index 83ff51ec1b4c2..322c2a4a049cf 100644 --- a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/write_user.ts +++ b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/write_user.ts @@ -7,21 +7,25 @@ import expect from '@kbn/expect'; import { countBy } from 'lodash'; +import { createApmApiSupertest } from '../../../common/apm_api_supertest'; import { registry } from '../../../common/registry'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function apiTest({ getService }: FtrProviderContext) { const apmWriteUser = getService('supertestAsApmWriteUser'); + const apmApiWriteUser = createApmApiSupertest(getService('supertestAsApmWriteUser')); function getJobs() { - return apmWriteUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); + return apmApiWriteUser({ endpoint: `GET /api/apm/settings/anomaly-detection/jobs` }); } function createJobs(environments: string[]) { - return apmWriteUser - .post(`/api/apm/settings/anomaly-detection/jobs`) - .send({ environments }) - .set('kbn-xsrf', 'foo'); + return apmApiWriteUser({ + endpoint: `POST /api/apm/settings/anomaly-detection/jobs`, + params: { + body: { environments }, + }, + }); } function deleteJobs(jobIds: string[]) { diff --git a/x-pack/test/apm_api_integration/tests/settings/custom_link.ts b/x-pack/test/apm_api_integration/tests/settings/custom_link.ts index 49b18be3580cf..c975a8219ddd3 100644 --- a/x-pack/test/apm_api_integration/tests/settings/custom_link.ts +++ b/x-pack/test/apm_api_integration/tests/settings/custom_link.ts @@ -5,15 +5,15 @@ * 2.0. */ -import URL from 'url'; import expect from '@kbn/expect'; import { CustomLink } from '../../../../plugins/apm/common/custom_link/custom_link_types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; +import { createApmApiSupertest } from '../../common/apm_api_supertest'; export default function customLinksTests({ getService }: FtrProviderContext) { - const supertestRead = getService('supertest'); - const supertestWrite = getService('supertestAsApmWriteUser'); + const supertestRead = createApmApiSupertest(getService('supertest')); + const supertestWrite = createApmApiSupertest(getService('supertestAsApmWriteUser')); const log = getService('log'); const archiveName = 'apm_8.0.0'; @@ -28,16 +28,16 @@ export default function customLinksTests({ getService }: FtrProviderContext) { { key: 'transaction.type', value: 'qux' }, ], } as CustomLink; - const response = await supertestWrite - .post(`/api/apm/settings/custom_links`) - .send(customLink) - .set('kbn-xsrf', 'foo'); - expect(response.status).to.be(403); - - expectSnapshot(response.body.message).toMatchInline( - `"To create custom links, you must be subscribed to an Elastic Gold license or above. With it, you'll have the ability to create custom links to improve your workflow when analyzing your services."` - ); + try { + await createCustomLink(customLink); + expect(true).to.be(false); + } catch (e) { + expect(e.res.status).to.be(403); + expectSnapshot(e.res.body.message).toMatchInline( + `"To create custom links, you must be subscribed to an Elastic Gold license or above. With it, you'll have the ability to create custom links to improve your workflow when analyzing your services."` + ); + } }); }); @@ -56,12 +56,13 @@ export default function customLinksTests({ getService }: FtrProviderContext) { } as CustomLink; await createCustomLink(customLink); }); + it('fetches a custom link', async () => { const { status, body } = await searchCustomLinks({ 'service.name': 'baz', 'transaction.type': 'qux', }); - const { label, url, filters } = body[0]; + const { label, url, filters } = body.customLinks[0]; expect(status).to.equal(200); expect({ label, url, filters }).to.eql({ @@ -73,13 +74,16 @@ export default function customLinksTests({ getService }: FtrProviderContext) { ], }); }); + it('updates a custom link', async () => { - let { status, body } = await searchCustomLinks({ + const { status, body } = await searchCustomLinks({ 'service.name': 'baz', 'transaction.type': 'qux', }); expect(status).to.equal(200); - await updateCustomLink(body[0].id, { + + const id = body.customLinks[0].id!; + await updateCustomLink(id, { label: 'foo', url: 'https://elastic.co?service.name={{service.name}}', filters: [ @@ -87,12 +91,14 @@ export default function customLinksTests({ getService }: FtrProviderContext) { { key: 'transaction.name', value: 'bar' }, ], }); - ({ status, body } = await searchCustomLinks({ + + const { status: newStatus, body: newBody } = await searchCustomLinks({ 'service.name': 'quz', 'transaction.name': 'bar', - })); - const { label, url, filters } = body[0]; - expect(status).to.equal(200); + }); + + const { label, url, filters } = newBody.customLinks[0]; + expect(newStatus).to.equal(200); expect({ label, url, filters }).to.eql({ label: 'foo', url: 'https://elastic.co?service.name={{service.name}}', @@ -102,84 +108,79 @@ export default function customLinksTests({ getService }: FtrProviderContext) { ], }); }); + it('deletes a custom link', async () => { - let { status, body } = await searchCustomLinks({ + const { status, body } = await searchCustomLinks({ 'service.name': 'quz', 'transaction.name': 'bar', }); expect(status).to.equal(200); - await deleteCustomLink(body[0].id); - ({ status, body } = await searchCustomLinks({ + expect(body.customLinks.length).to.be(1); + + const id = body.customLinks[0].id!; + await deleteCustomLink(id); + + const { status: newStatus, body: newBody } = await searchCustomLinks({ 'service.name': 'quz', 'transaction.name': 'bar', - })); - expect(status).to.equal(200); - expect(body).to.eql([]); + }); + expect(newStatus).to.equal(200); + expect(newBody.customLinks.length).to.be(0); }); - describe('transaction', () => { - it('fetches a transaction sample', async () => { - const response = await supertestRead.get( - '/api/apm/settings/custom_links/transaction?service.name=opbeans-java' - ); - expect(response.status).to.be(200); - expect(response.body.service.name).to.eql('opbeans-java'); + it('fetches a transaction sample', async () => { + const response = await supertestRead({ + endpoint: 'GET /api/apm/settings/custom_links/transaction', + params: { + query: { + 'service.name': 'opbeans-java', + }, + }, }); + expect(response.status).to.be(200); + expect(response.body.service.name).to.eql('opbeans-java'); }); } ); function searchCustomLinks(filters?: any) { - const path = URL.format({ - pathname: `/api/apm/settings/custom_links`, - query: filters, + return supertestRead({ + endpoint: 'GET /api/apm/settings/custom_links', + params: { + query: filters, + }, }); - return supertestRead.get(path).set('kbn-xsrf', 'foo'); } async function createCustomLink(customLink: CustomLink) { log.debug('creating configuration', customLink); - const res = await supertestWrite - .post(`/api/apm/settings/custom_links`) - .send(customLink) - .set('kbn-xsrf', 'foo'); - throwOnError(res); - - return res; + return supertestWrite({ + endpoint: 'POST /api/apm/settings/custom_links', + params: { + body: customLink, + }, + }); } async function updateCustomLink(id: string, customLink: CustomLink) { log.debug('updating configuration', id, customLink); - const res = await supertestWrite - .put(`/api/apm/settings/custom_links/${id}`) - .send(customLink) - .set('kbn-xsrf', 'foo'); - - throwOnError(res); - return res; + return supertestWrite({ + endpoint: 'PUT /api/apm/settings/custom_links/{id}', + params: { + path: { id }, + body: customLink, + }, + }); } async function deleteCustomLink(id: string) { log.debug('deleting configuration', id); - const res = await supertestWrite - .delete(`/api/apm/settings/custom_links/${id}`) - .set('kbn-xsrf', 'foo'); - - throwOnError(res); - - return res; - } - function throwOnError(res: any) { - const { statusCode, req, body } = res; - if (statusCode !== 200) { - throw new Error(` - Endpoint: ${req.method} ${req.path} - Service: ${JSON.stringify(res.request._data.service)} - Status code: ${statusCode} - Response: ${body.message}`); - } + return supertestWrite({ + endpoint: 'DELETE /api/apm/settings/custom_links/{id}', + params: { path: { id } }, + }); } }