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 1425d92ffee3..8d51a095200f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -416,4 +416,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 c24540a6ab4e..2ae3882ac3bd 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -32,6 +32,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 7f5e39620498..35df878c7492 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -8038,6 +8038,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 000000000000..fb7ef6d36ce2 --- /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 3316c74d52e3..4212e0430ff5 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 b785fcc7dab0..7df6ca343426 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 8ea4593bb89a..787b15d0a567 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 64600dd500bd..bc14bc153168 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 29f74b26d310..fdfed6eb0d68 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 11aab788ec7f..b4c78b54f329 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 de30af4a4707..c6f9c4efd98b 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 6c94b895f692..db5932a96fb1 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 5af7f0682db1..e21aaa08c432 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 e3e2a979c48d..d04bcb79a53e 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 c40f6ba2b885..8ae4c9dc0e01 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 5b448871804e..f932cec3cacb 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 d754710dc84f..ac1846155569 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 e762f517ce1b..71355a84d28d 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 4d2754a677bf..cd5fa5db89a3 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 081a3dbc907c..3e3bc892e651 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 bef0dfc22280..c098be41968d 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 8aa0c35f3671..3225951fd6c7 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 9722c99990e3..9d2b4bba22af 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 0dbc8f623534..77835afef863 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 4b4bc2e8feea..49fa3eab4786 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 6a11f862994e..bf9062418313 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 66fb72975ace..f31354bc7aa3 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 f4870439fe47..fc218f3ba6df 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 a4647bc148b1..4ff42b151dc8 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 c10ec1052f2a..13322b094c65 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 f2a169eb31f9..b88172a16206 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 23adbb23b232..94391b5b2fb0 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 3cd858aceaa9..4bc9764b704b 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 6f2910a2a5ef..a624c220a0e4 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 7d2e4a13278e..cbbf34c78c4a 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 b0ac35cc3667..b8d67f71a9ba 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 3b93cb1f402e..d94f2ce8f5c5 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 5f24c1ee2495..5bcf0d161653 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 8009f288d48c..2c9601d709cb 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 f55389ec2d5f..23016cc5dd8e 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 6c46580f4738..31d18b7a9709 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 2bd3fef8c0e8..1018b9eca211 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 024deca55849..9a910787d5fe 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 5740e47d0076..382053f13395 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 f9e72bff231f..f33421253677 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 cdd9cb5b08a3..5f0be1b6fadb 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 25d34b5d102f..56146c49fc57 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 b0bae6aa91a3..1821e92ee5a7 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 6d630ede1cb1..55ead8d942ac 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 f5106fce78cc..f623872303c5 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 c6d55a85dd70..b0cce3296fe2 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 b125407a160a..ef2675f4f6c6 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 50613c10ff7c..88b1cf3a344e 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 aa41880fba44..1f0aa401bcab 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 e20103cc6ddc..b8a14253a229 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 2e83baece01a..45e17c167851 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 5c188ff0d093..0a34711b9b40 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 51f386d59c04..c0707d028618 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 60fb9a8bfa85..fff661250c6d 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 8d0acb7f85f5..0b7f82c0b838 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 abdc8da78502..bbe13874d7d3 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 32f2238b0dde..3bebcd49ec34 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 01d279764180..9958b8dec012 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 46f2628cc73d..13e70a2043cf 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 4d30e706cdd5..d74aac0992eb 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 448591f7e143..4aa7d7e6d412 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 710e614165aa..f69d3fc9631d 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 ed1354a21916..fd7d2120ab6f 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 1a1fa799639b..b9c0a76b6fb9 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 ecf56e2aec24..3156acb469a7 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 e65b0b679da5..e9060688c63a 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 7ba19035a90b..b4d25ca8b2a0 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 e3ed398171d0..31e8d6cc1e9f 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 e5922d9ed3e9..de7f35c4081b 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 0d47579f50ae..91057c97579e 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 fc217bef772d..a6ab553f0941 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 4d3e07040f76..1575041fb2f4 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 05abac80b67c..cb6ea799078a 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 f473ed963c75..35443ca09007 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 3123ce96114d..43041280d028 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 2b310a5241a6..f59f1939b598 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 4b7868344429..a91ff3d3b0fa 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 a7e02adc0401..29a7a06f1530 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 8e9846849695..491d20b929d2 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 13ff3d3be3c0..77f265d0b81e 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 3663c42ad4ab..94b50386ac21 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 6144825de1a7..eefde9067731 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 cc2647c347b3..e94198ee4e06 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 5414f45e2909..354c57c36511 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 69de7eba6e75..db111390cfaf 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 04f4e6cd3c18..0178fd770f9c 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 2b056498d7f1..ab8a01cfb9c3 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 9b5bffc380c2..31555be25b2f 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 8c80c4d512b5..67b106fdf681 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 a1523fae9d4a..c326037b9ecb 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 391770120c49..ef1e0a07c639 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 000000000000..76eab7ab85cf --- /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 adf6fcfbe981..04ce83323ee6 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 58da2e2b0df5..3712b49ce169 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 e21069ba2f0a..9e4a708fae30 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 c0f92f5f2acc..57018b5012aa 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 edeffe1e5c29..553f22fc2279 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 49a568e0051a..9f0f1b15c058 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 000000000000..aae2e38e8ec8 --- /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 a86063bcab58..c6bdce217e22 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 fde121055181..142802840974 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 9cf11068f0a2..aac92685a3c3 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 2c7313e4d01e..94188b632177 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 14a5cf66c409..fbd60c0f1ab1 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 83ff51ec1b4c..322c2a4a049c 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 49b18be3580c..c975a8219ddd 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 } }, + }); } }