diff --git a/docs/getting-started/quick-start-guide.asciidoc b/docs/getting-started/quick-start-guide.asciidoc index d614aece5b425..2bddd9bf61452 100644 --- a/docs/getting-started/quick-start-guide.asciidoc +++ b/docs/getting-started/quick-start-guide.asciidoc @@ -138,7 +138,7 @@ image::images/dashboard_sampleDataAddFilter_7.15.0.png[The [eCommerce] Revenue D [[quick-start-whats-next]] == What's next? -*Add your own data.* Ready to add your own data? Go to {fleet-guide}/fleet-quick-start.html[Quick start: Get logs and metrics into the Elastic Stack] to learn how to ingest your data, or go to <> and learn about all the other ways you can add data. +*Add your own data.* Ready to add your own data? Go to {observability-guide}/ingest-logs-metrics-uptime.html[Ingest logs, metrics, and uptime data with {agent}], or go to <> and learn about all the other ways you can add data. *Explore your own data in Discover.* Ready to learn more about exploring your data in *Discover*? Go to <>. diff --git a/docs/osquery/osquery.asciidoc b/docs/osquery/osquery.asciidoc index 1e4e6604a7c70..a4f3c80463143 100644 --- a/docs/osquery/osquery.asciidoc +++ b/docs/osquery/osquery.asciidoc @@ -365,7 +365,7 @@ The following is an example of an **error response** for an undefined action que == System requirements * {fleet-guide}/fleet-overview.html[Fleet] is enabled on your cluster, and -one or more {fleet-guide}/elastic-agent-installation-configuration.html[Elastic Agents] is enrolled. +one or more {fleet-guide}/elastic-agent-installation.html[Elastic Agents] is enrolled. * The https://docs.elastic.co/en/integrations/osquery_manager[*Osquery Manager*] integration has been added and configured for an agent policy through Fleet. diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index c7f745648cbe9..ad38ac1710fd5 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -47,7 +47,7 @@ so you can quickly get insights into your data, and {fleet} mode offers several image::images/addData_fleet_7.15.0.png[Add data using Fleet] To get started, refer to -{fleet-guide}/fleet-quick-start.html[Quick start: Get logs and metrics into the Elastic Stack]. +{observability-guide}/ingest-logs-metrics-uptime.html[Ingest logs, metrics, and uptime data with {agent}]. [discrete] [[upload-data-kibana]] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 16fa8eb734204..4802a4da8182c 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -275,7 +275,7 @@ that the {kib} server uses to perform maintenance on the {kib} index at startup. is an alternative to `elasticsearch.username` and `elasticsearch.password`. | `enterpriseSearch.host` - | The URL of your Enterprise Search instance + | The http(s) URL of your Enterprise Search instance. For example, in a local self-managed setup, set this to `http://localhost:3002`. Authentication between Kibana and the Enterprise Search host URL, such as via OAuth, is not supported. You can also {enterprise-search-ref}/configure-ssl-tls.html#configure-ssl-tls-in-kibana[configure Kibana to trust your Enterprise Search TLS certificate authority]. | `interpreter.enableInVisualize` | Enables use of interpreter in Visualize. *Default: `true`* diff --git a/src/plugins/custom_integrations/common/index.ts b/src/plugins/custom_integrations/common/index.ts index 944ac6ba3e6ee..98148bb22c816 100755 --- a/src/plugins/custom_integrations/common/index.ts +++ b/src/plugins/custom_integrations/common/index.ts @@ -49,6 +49,7 @@ export const INTEGRATION_CATEGORY_DISPLAY = { project_management: 'Project Management', software_development: 'Software Development', upload_file: 'Upload a file', + website_search: 'Website Search', }; /** diff --git a/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts index 266d7246c35d4..28ce2ff24b961 100644 --- a/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts +++ b/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { - FieldValuePair, - HistogramItem, - RawResponseBase, - SearchStrategyClientParams, -} from '../types'; +import { FieldValuePair, HistogramItem } from '../types'; import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from './constants'; import { FieldStats } from '../field_stats_types'; @@ -33,11 +28,7 @@ export interface FailedTransactionsCorrelationsParams { percentileThreshold: number; } -export type FailedTransactionsCorrelationsRequestParams = - FailedTransactionsCorrelationsParams & SearchStrategyClientParams; - -export interface FailedTransactionsCorrelationsRawResponse - extends RawResponseBase { +export interface FailedTransactionsCorrelationsRawResponse { log: string[]; failedTransactionsCorrelations?: FailedTransactionsCorrelation[]; percentileThresholdValue?: number; diff --git a/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts index 2eb2b37159459..ea74175a3dacb 100644 --- a/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts +++ b/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { - FieldValuePair, - HistogramItem, - RawResponseBase, - SearchStrategyClientParams, -} from '../types'; +import { FieldValuePair, HistogramItem } from '../types'; import { FieldStats } from '../field_stats_types'; export interface LatencyCorrelation extends FieldValuePair { @@ -33,10 +28,7 @@ export interface LatencyCorrelationsParams { analyzeCorrelations: boolean; } -export type LatencyCorrelationsRequestParams = LatencyCorrelationsParams & - SearchStrategyClientParams; - -export interface LatencyCorrelationsRawResponse extends RawResponseBase { +export interface LatencyCorrelationsRawResponse { log: string[]; overallHistogram?: HistogramItem[]; percentileThresholdValue?: number; diff --git a/x-pack/plugins/apm/common/search_strategies/types.ts b/x-pack/plugins/apm/common/search_strategies/types.ts index d7c6eab1f07c1..ff925f70fc9b0 100644 --- a/x-pack/plugins/apm/common/search_strategies/types.ts +++ b/x-pack/plugins/apm/common/search_strategies/types.ts @@ -31,16 +31,26 @@ export interface RawResponseBase { took: number; } -export interface SearchStrategyClientParams { +export interface SearchStrategyClientParamsBase { environment: string; kuery: string; serviceName?: string; transactionName?: string; transactionType?: string; +} + +export interface RawSearchStrategyClientParams + extends SearchStrategyClientParamsBase { start?: string; end?: string; } +export interface SearchStrategyClientParams + extends SearchStrategyClientParamsBase { + start: number; + end: number; +} + export interface SearchStrategyServerParams { index: string; includeFrozen?: boolean; diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx index 9956452c565b3..918f94e64ef09 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx @@ -19,6 +19,7 @@ import type { IKibanaSearchResponse } from 'src/plugins/data/public'; import { EuiThemeProvider } from 'src/plugins/kibana_react/common'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; import type { LatencyCorrelationsRawResponse } from '../../../../common/search_strategies/latency_correlations/types'; +import type { RawResponseBase } from '../../../../common/search_strategies/types'; import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { @@ -34,7 +35,9 @@ function Wrapper({ dataSearchResponse, }: { children?: ReactNode; - dataSearchResponse: IKibanaSearchResponse; + dataSearchResponse: IKibanaSearchResponse< + LatencyCorrelationsRawResponse & RawResponseBase + >; }) { const mockDataSearch = jest.fn(() => of(dataSearchResponse)); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx index bd0ff4c87c3be..0e9639de4aa74 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx @@ -8,43 +8,24 @@ import { render, screen, waitFor } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import React, { ReactNode } from 'react'; -import { of } from 'rxjs'; import { CoreStart } from 'kibana/public'; import { merge } from 'lodash'; -import { dataPluginMock } from 'src/plugins/data/public/mocks'; -import type { IKibanaSearchResponse } from 'src/plugins/data/public'; import { EuiThemeProvider } from 'src/plugins/kibana_react/common'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; -import type { LatencyCorrelationsRawResponse } from '../../../../../common/search_strategies/latency_correlations/types'; import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider'; import { ApmPluginContextValue } from '../../../../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, } from '../../../../context/apm_plugin/mock_apm_plugin_context'; +import * as useFetcherModule from '../../../../hooks/use_fetcher'; import { fromQuery } from '../../../shared/Links/url_helpers'; import { getFormattedSelection, TransactionDistribution } from './index'; -function Wrapper({ - children, - dataSearchResponse, -}: { - children?: ReactNode; - dataSearchResponse: IKibanaSearchResponse; -}) { - const mockDataSearch = jest.fn(() => of(dataSearchResponse)); - - const dataPluginMockStart = dataPluginMock.createStartContract(); +function Wrapper({ children }: { children?: ReactNode }) { const KibanaReactContext = createKibanaReactContext({ - data: { - ...dataPluginMockStart, - search: { - ...dataPluginMockStart.search, - search: mockDataSearch, - }, - }, usageCollection: { reportUiCounter: () => {} }, } as Partial); @@ -105,18 +86,14 @@ describe('transaction_details/distribution', () => { describe('TransactionDistribution', () => { it('shows loading indicator when the service is running and returned no results yet', async () => { + jest.spyOn(useFetcherModule, 'useFetcher').mockImplementation(() => ({ + data: {}, + refetch: () => {}, + status: useFetcherModule.FETCH_STATUS.LOADING, + })); + render( - + { }); it("doesn't show loading indicator when the service isn't running", async () => { + jest.spyOn(useFetcherModule, 'useFetcher').mockImplementation(() => ({ + data: { percentileThresholdValue: 1234, overallHistogram: [] }, + refetch: () => {}, + status: useFetcherModule.FETCH_STATUS.SUCCESS, + })); + render( - + { + if (serviceName && environment && start && end) { + return callApmApi({ + endpoint: 'GET /internal/apm/latency/overall_distribution', + params: { + query: { + serviceName, + transactionName, + transactionType, + kuery, + environment, + start, + end, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + }, + }, + }); + } + }, + [ + serviceName, + transactionName, + transactionType, + kuery, + environment, + start, + end, + ] ); + const overallHistogram = + data.overallHistogram === undefined && status !== FETCH_STATUS.LOADING + ? [] + : data.overallHistogram; + const hasData = + Array.isArray(overallHistogram) && overallHistogram.length > 0; + useEffect(() => { - if (isErrorMessage(progress.error)) { + if (isErrorMessage(error)) { notifications.toasts.addDanger({ title: i18n.translate( 'xpack.apm.transactionDetails.distribution.errorTitle', @@ -119,10 +156,10 @@ export function TransactionDistribution({ defaultMessage: 'An error occurred fetching the distribution', } ), - text: progress.error.toString(), + text: error.toString(), }); } - }, [progress.error, notifications.toasts]); + }, [error, notifications.toasts]); const trackApmEvent = useUiTracker({ app: 'apm' }); @@ -213,7 +250,7 @@ export function TransactionDistribution({ data={transactionDistributionChartData} markerCurrentTransaction={markerCurrentTransaction} markerPercentile={DEFAULT_PERCENTILE_THRESHOLD} - markerValue={response.percentileThresholdValue ?? 0} + markerValue={data.percentileThresholdValue ?? 0} onChartSelection={onTrackedChartSelection as BrushEndListener} hasData={hasData} selection={selection} diff --git a/x-pack/plugins/apm/public/hooks/use_search_strategy.ts b/x-pack/plugins/apm/public/hooks/use_search_strategy.ts index ca8d28b106f84..275eddb68ae00 100644 --- a/x-pack/plugins/apm/public/hooks/use_search_strategy.ts +++ b/x-pack/plugins/apm/public/hooks/use_search_strategy.ts @@ -16,7 +16,7 @@ import { } from '../../../../../src/plugins/data/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; -import type { SearchStrategyClientParams } from '../../common/search_strategies/types'; +import type { RawSearchStrategyClientParams } from '../../common/search_strategies/types'; import type { RawResponseBase } from '../../common/search_strategies/types'; import type { LatencyCorrelationsParams, @@ -77,13 +77,15 @@ interface SearchStrategyReturnBase { export function useSearchStrategy( searchStrategyName: typeof APM_SEARCH_STRATEGIES.APM_LATENCY_CORRELATIONS, searchStrategyParams: LatencyCorrelationsParams -): SearchStrategyReturnBase; +): SearchStrategyReturnBase; // Function overload for Failed Transactions Correlations export function useSearchStrategy( searchStrategyName: typeof APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS, searchStrategyParams: FailedTransactionsCorrelationsParams -): SearchStrategyReturnBase; +): SearchStrategyReturnBase< + FailedTransactionsCorrelationsRawResponse & RawResponseBase +>; export function useSearchStrategy< TRawResponse extends RawResponseBase, @@ -145,7 +147,7 @@ export function useSearchStrategy< // Submit the search request using the `data.search` service. searchSubscription$.current = data.search .search< - IKibanaSearchRequest, + IKibanaSearchRequest, IKibanaSearchResponse >(request, { strategy: searchStrategyName, diff --git a/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts b/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts new file mode 100644 index 0000000000000..39470869488c3 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; + +import { ProcessorEvent } from '../../../common/processor_event'; + +import { withApmSpan } from '../../utils/with_apm_span'; + +import { + getHistogramIntervalRequest, + getHistogramRangeSteps, +} from '../search_strategies/queries/query_histogram_range_steps'; +import { getTransactionDurationRangesRequest } from '../search_strategies/queries/query_ranges'; + +import { getPercentileThresholdValue } from './get_percentile_threshold_value'; +import type { + OverallLatencyDistributionOptions, + OverallLatencyDistributionResponse, +} from './types'; + +export async function getOverallLatencyDistribution( + options: OverallLatencyDistributionOptions +) { + return withApmSpan('get_overall_latency_distribution', async () => { + const overallLatencyDistribution: OverallLatencyDistributionResponse = { + log: [], + }; + + const { setup, ...rawParams } = options; + const { apmEventClient } = setup; + const params = { + // pass on an empty index because we're using only the body attribute + // of the request body getters we're reusing from search strategies. + index: '', + ...rawParams, + }; + + // #1: get 95th percentile to be displayed as a marker in the log log chart + overallLatencyDistribution.percentileThresholdValue = + await getPercentileThresholdValue(options); + + // finish early if we weren't able to identify the percentileThresholdValue. + if (!overallLatencyDistribution.percentileThresholdValue) { + return overallLatencyDistribution; + } + + // #2: get histogram range steps + const steps = 100; + + const { body: histogramIntervalRequestBody } = + getHistogramIntervalRequest(params); + + const histogramIntervalResponse = (await apmEventClient.search( + 'get_histogram_interval', + { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: histogramIntervalRequestBody, + } + )) as { + aggregations?: { + transaction_duration_min: estypes.AggregationsValueAggregate; + transaction_duration_max: estypes.AggregationsValueAggregate; + }; + hits: { total: estypes.SearchTotalHits }; + }; + + if ( + !histogramIntervalResponse.aggregations || + histogramIntervalResponse.hits.total.value === 0 + ) { + return overallLatencyDistribution; + } + + const min = + histogramIntervalResponse.aggregations.transaction_duration_min.value; + const max = + histogramIntervalResponse.aggregations.transaction_duration_max.value * 2; + + const histogramRangeSteps = getHistogramRangeSteps(min, max, steps); + + // #3: get histogram chart data + const { body: transactionDurationRangesRequestBody } = + getTransactionDurationRangesRequest(params, histogramRangeSteps); + + const transactionDurationRangesResponse = (await apmEventClient.search( + 'get_transaction_duration_ranges', + { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: transactionDurationRangesRequestBody, + } + )) as { + aggregations?: { + logspace_ranges: estypes.AggregationsMultiBucketAggregate<{ + from: number; + doc_count: number; + }>; + }; + }; + + if (!transactionDurationRangesResponse.aggregations) { + return overallLatencyDistribution; + } + + overallLatencyDistribution.overallHistogram = + transactionDurationRangesResponse.aggregations.logspace_ranges.buckets + .map((d) => ({ + key: d.from, + doc_count: d.doc_count, + })) + .filter((d) => d.key !== undefined); + + return overallLatencyDistribution; + }); +} diff --git a/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts b/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts new file mode 100644 index 0000000000000..0d417a370e0b6 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; + +import { ProcessorEvent } from '../../../common/processor_event'; + +import { getTransactionDurationPercentilesRequest } from '../search_strategies/queries/query_percentiles'; + +import type { OverallLatencyDistributionOptions } from './types'; + +export async function getPercentileThresholdValue( + options: OverallLatencyDistributionOptions +) { + const { setup, percentileThreshold, ...rawParams } = options; + const { apmEventClient } = setup; + const params = { + // pass on an empty index because we're using only the body attribute + // of the request body getters we're reusing from search strategies. + index: '', + ...rawParams, + }; + + const { body: transactionDurationPercentilesRequestBody } = + getTransactionDurationPercentilesRequest(params, [percentileThreshold]); + + const transactionDurationPercentilesResponse = (await apmEventClient.search( + 'get_transaction_duration_percentiles', + { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: transactionDurationPercentilesRequestBody, + } + )) as { + aggregations?: { + transaction_duration_percentiles: estypes.AggregationsTDigestPercentilesAggregate; + }; + }; + + if (!transactionDurationPercentilesResponse.aggregations) { + return; + } + + const percentilesResponseThresholds = + transactionDurationPercentilesResponse.aggregations + .transaction_duration_percentiles?.values ?? {}; + + return percentilesResponseThresholds[`${percentileThreshold}.0`]; +} diff --git a/x-pack/plugins/apm/server/lib/latency/types.ts b/x-pack/plugins/apm/server/lib/latency/types.ts new file mode 100644 index 0000000000000..8dad1a39bd159 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/latency/types.ts @@ -0,0 +1,23 @@ +/* + * 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 { Setup } from '../helpers/setup_request'; +import { CorrelationsOptions } from '../search_strategies/queries/get_filters'; + +export interface OverallLatencyDistributionOptions extends CorrelationsOptions { + percentileThreshold: number; + setup: Setup; +} + +export interface OverallLatencyDistributionResponse { + log: string[]; + percentileThresholdValue?: number; + overallHistogram?: Array<{ + key: number; + doc_count: number; + }>; +} diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts index af5e535abdc3f..efc28ce98e5e0 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts @@ -9,17 +9,15 @@ import { chunk } from 'lodash'; import type { ElasticsearchClient } from 'src/core/server'; -import type { ISearchStrategy } from '../../../../../../../src/plugins/data/server'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, -} from '../../../../../../../src/plugins/data/common'; - import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; import { EventOutcome } from '../../../../common/event_outcome'; -import type { SearchStrategyServerParams } from '../../../../common/search_strategies/types'; import type { - FailedTransactionsCorrelationsRequestParams, + SearchStrategyClientParams, + SearchStrategyServerParams, + RawResponseBase, +} from '../../../../common/search_strategies/types'; +import type { + FailedTransactionsCorrelationsParams, FailedTransactionsCorrelationsRawResponse, } from '../../../../common/search_strategies/failed_transactions_correlations/types'; import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; @@ -38,22 +36,18 @@ import { failedTransactionsCorrelationsSearchServiceStateProvider } from './fail import { ERROR_CORRELATION_THRESHOLD } from '../constants'; import { fetchFieldsStats } from '../queries/field_stats/get_fields_stats'; -export type FailedTransactionsCorrelationsSearchServiceProvider = +type FailedTransactionsCorrelationsSearchServiceProvider = SearchServiceProvider< - FailedTransactionsCorrelationsRequestParams, - FailedTransactionsCorrelationsRawResponse + FailedTransactionsCorrelationsParams & SearchStrategyClientParams, + FailedTransactionsCorrelationsRawResponse & RawResponseBase >; -export type FailedTransactionsCorrelationsSearchStrategy = ISearchStrategy< - IKibanaSearchRequest, - IKibanaSearchResponse ->; - export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransactionsCorrelationsSearchServiceProvider = ( esClient: ElasticsearchClient, getApmIndices: () => Promise, - searchServiceParams: FailedTransactionsCorrelationsRequestParams, + searchServiceParams: FailedTransactionsCorrelationsParams & + SearchStrategyClientParams, includeFrozen: boolean ) => { const { addLogMessage, getLogMessages } = searchServiceLogProvider(); @@ -63,7 +57,8 @@ export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransact async function fetchErrorCorrelations() { try { const indices = await getApmIndices(); - const params: FailedTransactionsCorrelationsRequestParams & + const params: FailedTransactionsCorrelationsParams & + SearchStrategyClientParams & SearchStrategyServerParams = { ...searchServiceParams, index: indices.transaction, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts index ec91165cb481b..4763cd994d309 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts @@ -5,8 +5,4 @@ * 2.0. */ -export { - failedTransactionsCorrelationsSearchServiceProvider, - FailedTransactionsCorrelationsSearchServiceProvider, - FailedTransactionsCorrelationsSearchStrategy, -} from './failed_transactions_correlations_search_service'; +export { failedTransactionsCorrelationsSearchServiceProvider } from './failed_transactions_correlations_search_service'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts index 073bb122896ff..040aa5a7e424e 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts @@ -5,8 +5,4 @@ * 2.0. */ -export { - latencyCorrelationsSearchServiceProvider, - LatencyCorrelationsSearchServiceProvider, - LatencyCorrelationsSearchStrategy, -} from './latency_correlations_search_service'; +export { latencyCorrelationsSearchServiceProvider } from './latency_correlations_search_service'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts index 4862f7dd1de1a..f170818d018d4 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts @@ -8,15 +8,13 @@ import { range } from 'lodash'; import type { ElasticsearchClient } from 'src/core/server'; -import type { ISearchStrategy } from '../../../../../../../src/plugins/data/server'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, -} from '../../../../../../../src/plugins/data/common'; - -import type { SearchStrategyServerParams } from '../../../../common/search_strategies/types'; import type { - LatencyCorrelationsRequestParams, + RawResponseBase, + SearchStrategyClientParams, + SearchStrategyServerParams, +} from '../../../../common/search_strategies/types'; +import type { + LatencyCorrelationsParams, LatencyCorrelationsRawResponse, } from '../../../../common/search_strategies/latency_correlations/types'; @@ -38,21 +36,16 @@ import type { SearchServiceProvider } from '../search_strategy_provider'; import { latencyCorrelationsSearchServiceStateProvider } from './latency_correlations_search_service_state'; import { fetchFieldsStats } from '../queries/field_stats/get_fields_stats'; -export type LatencyCorrelationsSearchServiceProvider = SearchServiceProvider< - LatencyCorrelationsRequestParams, - LatencyCorrelationsRawResponse ->; - -export type LatencyCorrelationsSearchStrategy = ISearchStrategy< - IKibanaSearchRequest, - IKibanaSearchResponse +type LatencyCorrelationsSearchServiceProvider = SearchServiceProvider< + LatencyCorrelationsParams & SearchStrategyClientParams, + LatencyCorrelationsRawResponse & RawResponseBase >; export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearchServiceProvider = ( esClient: ElasticsearchClient, getApmIndices: () => Promise, - searchServiceParams: LatencyCorrelationsRequestParams, + searchServiceParams: LatencyCorrelationsParams & SearchStrategyClientParams, includeFrozen: boolean ) => { const { addLogMessage, getLogMessages } = searchServiceLogProvider(); @@ -61,7 +54,9 @@ export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearch async function fetchCorrelations() { let params: - | (LatencyCorrelationsRequestParams & SearchStrategyServerParams) + | (LatencyCorrelationsParams & + SearchStrategyClientParams & + SearchStrategyServerParams) | undefined; try { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts index deb89ace47c5d..d3cee1c4ca596 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts @@ -15,8 +15,8 @@ import { fetchFieldsStats } from './get_fields_stats'; const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts index c77b4df78f865..9d0441e513198 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts @@ -14,8 +14,8 @@ describe('correlations', () => { const query = getQueryWithParams({ params: { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', @@ -45,8 +45,8 @@ describe('correlations', () => { index: 'apm-*', serviceName: 'actualServiceName', transactionName: 'actualTransactionName', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, environment: 'dev', kuery: '', includeFrozen: false, @@ -93,8 +93,8 @@ describe('correlations', () => { const query = getQueryWithParams({ params: { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts index f00c89503f103..31a98b0a6bb18 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts @@ -6,15 +6,10 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { getOrElse } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; -import * as t from 'io-ts'; -import { failure } from 'io-ts/lib/PathReporter'; import type { FieldValuePair, SearchStrategyParams, } from '../../../../common/search_strategies/types'; -import { rangeRt } from '../../../routes/default_api_types'; import { getCorrelationsFilters } from './get_filters'; export const getTermsQuery = ({ fieldName, fieldValue }: FieldValuePair) => { @@ -36,22 +31,14 @@ export const getQueryWithParams = ({ params, termFilters }: QueryParams) => { transactionName, } = params; - // converts string based start/end to epochmillis - const decodedRange = pipe( - rangeRt.decode({ start, end }), - getOrElse((errors) => { - throw new Error(failure(errors).join('\n')); - }) - ); - const correlationFilters = getCorrelationsFilters({ environment, kuery, serviceName, transactionType, transactionName, - start: decodedRange.start, - end: decodedRange.end, + start, + end, }); return { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts index fd5f52207d4c5..eb771e1e1aaf4 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts @@ -16,6 +16,8 @@ describe('correlations', () => { includeFrozen: true, environment: ENVIRONMENT_ALL.value, kuery: '', + start: 1577836800000, + end: 1609459200000, }); expect(requestBase).toEqual({ index: 'apm-*', @@ -29,6 +31,8 @@ describe('correlations', () => { index: 'apm-*', environment: ENVIRONMENT_ALL.value, kuery: '', + start: 1577836800000, + end: 1609459200000, }); expect(requestBase).toEqual({ index: 'apm-*', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts index fc2dacce61a73..40fcc17444492 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts @@ -18,8 +18,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts index 6e0521ac1a008..bae42666e6db0 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts @@ -20,8 +20,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts index 9ffbf6b2ce18d..ab7a0b4e02072 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts @@ -20,8 +20,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts index daf6b368c78b1..9c704ef7b489a 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts @@ -17,8 +17,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts index 7ecb1d2d8a333..7cc6106f671a7 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts @@ -17,8 +17,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts index ffc86c7ef6c32..41a2fa9a5039e 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts @@ -17,8 +17,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts index 790919d193028..439bb9e4b9cd6 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts @@ -17,7 +17,11 @@ import type { SearchStrategyParams } from '../../../../common/search_strategies/ import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; -const getHistogramRangeSteps = (min: number, max: number, steps: number) => { +export const getHistogramRangeSteps = ( + min: number, + max: number, + steps: number +) => { // A d3 based scale function as a helper to get equally distributed bins on a log scale. // We round the final values because the ES range agg we use won't accept numbers with decimals for `transaction.duration.us`. const logFn = scaleLog().domain([min, max]).range([1, steps]); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts index 375e32b1472c6..00e8c26497eb2 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts @@ -17,8 +17,8 @@ import { fetchTransactionDurationHistograms } from './query_histograms_generator const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts index ce86ffd9654e6..57e3e6cadb9bc 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts @@ -17,8 +17,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts index e210eb7d41e78..7d67e80ae3398 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts @@ -17,8 +17,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts index 8a9d04df32036..034bd2a60ad19 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts @@ -13,7 +13,7 @@ import { IKibanaSearchRequest } from '../../../../../../src/plugins/data/common' import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import type { LatencyCorrelationsParams } from '../../../common/search_strategies/latency_correlations/types'; -import type { SearchStrategyClientParams } from '../../../common/search_strategies/types'; +import type { RawSearchStrategyClientParams } from '../../../common/search_strategies/types'; import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; @@ -112,7 +112,7 @@ describe('APM Correlations search strategy', () => { let mockDeps: SearchStrategyDependencies; let params: Required< IKibanaSearchRequest< - LatencyCorrelationsParams & SearchStrategyClientParams + LatencyCorrelationsParams & RawSearchStrategyClientParams > >['params']; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts index cec10294460b0..8035e9e4d97ca 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts @@ -7,6 +7,10 @@ import uuid from 'uuid'; import { of } from 'rxjs'; +import { getOrElse } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as t from 'io-ts'; +import { failure } from 'io-ts/lib/PathReporter'; import type { ElasticsearchClient } from 'src/core/server'; @@ -16,18 +20,21 @@ import { IKibanaSearchResponse, } from '../../../../../../src/plugins/data/common'; -import type { SearchStrategyClientParams } from '../../../common/search_strategies/types'; -import type { RawResponseBase } from '../../../common/search_strategies/types'; -import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; - import type { - LatencyCorrelationsSearchServiceProvider, - LatencyCorrelationsSearchStrategy, -} from './latency_correlations'; + RawResponseBase, + RawSearchStrategyClientParams, + SearchStrategyClientParams, +} from '../../../common/search_strategies/types'; +import type { + LatencyCorrelationsParams, + LatencyCorrelationsRawResponse, +} from '../../../common/search_strategies/latency_correlations/types'; import type { - FailedTransactionsCorrelationsSearchServiceProvider, - FailedTransactionsCorrelationsSearchStrategy, -} from './failed_transactions_correlations'; + FailedTransactionsCorrelationsParams, + FailedTransactionsCorrelationsRawResponse, +} from '../../../common/search_strategies/failed_transactions_correlations/types'; +import { rangeRt } from '../../routes/default_api_types'; +import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; interface SearchServiceState { cancel: () => void; @@ -56,35 +63,50 @@ export type SearchServiceProvider< // Failed Transactions Correlations function overload export function searchStrategyProvider( - searchServiceProvider: FailedTransactionsCorrelationsSearchServiceProvider, + searchServiceProvider: SearchServiceProvider< + FailedTransactionsCorrelationsParams & SearchStrategyClientParams, + FailedTransactionsCorrelationsRawResponse & RawResponseBase + >, getApmIndices: () => Promise, includeFrozen: boolean -): FailedTransactionsCorrelationsSearchStrategy; +): ISearchStrategy< + IKibanaSearchRequest< + FailedTransactionsCorrelationsParams & RawSearchStrategyClientParams + >, + IKibanaSearchResponse< + FailedTransactionsCorrelationsRawResponse & RawResponseBase + > +>; // Latency Correlations function overload export function searchStrategyProvider( - searchServiceProvider: LatencyCorrelationsSearchServiceProvider, + searchServiceProvider: SearchServiceProvider< + LatencyCorrelationsParams & SearchStrategyClientParams, + LatencyCorrelationsRawResponse & RawResponseBase + >, getApmIndices: () => Promise, includeFrozen: boolean -): LatencyCorrelationsSearchStrategy; +): ISearchStrategy< + IKibanaSearchRequest< + LatencyCorrelationsParams & RawSearchStrategyClientParams + >, + IKibanaSearchResponse +>; -export function searchStrategyProvider< - TSearchStrategyClientParams extends SearchStrategyClientParams, - TRawResponse extends RawResponseBase ->( +export function searchStrategyProvider( searchServiceProvider: SearchServiceProvider< - TSearchStrategyClientParams, - TRawResponse + TRequestParams & SearchStrategyClientParams, + TResponseParams & RawResponseBase >, getApmIndices: () => Promise, includeFrozen: boolean ): ISearchStrategy< - IKibanaSearchRequest, - IKibanaSearchResponse + IKibanaSearchRequest, + IKibanaSearchResponse > { const searchServiceMap = new Map< string, - GetSearchServiceState + GetSearchServiceState >(); return { @@ -93,9 +115,21 @@ export function searchStrategyProvider< throw new Error('Invalid request parameters.'); } + const { start: startString, end: endString } = request.params; + + // converts string based start/end to epochmillis + const decodedRange = pipe( + rangeRt.decode({ start: startString, end: endString }), + getOrElse((errors) => { + throw new Error(failure(errors).join('\n')); + }) + ); + // The function to fetch the current state of the search service. // This will be either an existing service for a follow up fetch or a new one for new requests. - let getSearchServiceState: GetSearchServiceState; + let getSearchServiceState: GetSearchServiceState< + TResponseParams & RawResponseBase + >; // If the request includes an ID, we require that the search service already exists // otherwise we throw an error. The client should never poll a service that's been cancelled or finished. @@ -111,10 +145,30 @@ export function searchStrategyProvider< getSearchServiceState = existingGetSearchServiceState; } else { + const { + start, + end, + environment, + kuery, + serviceName, + transactionName, + transactionType, + ...requestParams + } = request.params; + getSearchServiceState = searchServiceProvider( deps.esClient.asCurrentUser, getApmIndices, - request.params as TSearchStrategyClientParams, + { + environment, + kuery, + serviceName, + transactionName, + transactionType, + start: decodedRange.start, + end: decodedRange.end, + ...(requestParams as unknown as TRequestParams), + }, includeFrozen ); } diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts index 472e46fecfa10..3fa6152d953f3 100644 --- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -17,6 +17,7 @@ import { environmentsRouteRepository } from './environments'; import { errorsRouteRepository } from './errors'; import { apmFleetRouteRepository } from './fleet'; import { indexPatternRouteRepository } from './index_pattern'; +import { latencyDistributionRouteRepository } from './latency_distribution'; import { metricsRouteRepository } from './metrics'; import { observabilityOverviewRouteRepository } from './observability_overview'; import { rumRouteRepository } from './rum_client'; @@ -41,6 +42,7 @@ const getTypedGlobalApmServerRouteRepository = () => { .merge(indexPatternRouteRepository) .merge(environmentsRouteRepository) .merge(errorsRouteRepository) + .merge(latencyDistributionRouteRepository) .merge(metricsRouteRepository) .merge(observabilityOverviewRouteRepository) .merge(rumRouteRepository) diff --git a/x-pack/plugins/apm/server/routes/latency_distribution.ts b/x-pack/plugins/apm/server/routes/latency_distribution.ts new file mode 100644 index 0000000000000..ea921a7f4838d --- /dev/null +++ b/x-pack/plugins/apm/server/routes/latency_distribution.ts @@ -0,0 +1,63 @@ +/* + * 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 * as t from 'io-ts'; +import { toNumberRt } from '@kbn/io-ts-utils'; +import { getOverallLatencyDistribution } from '../lib/latency/get_overall_latency_distribution'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { environmentRt, kueryRt, rangeRt } from './default_api_types'; + +const latencyOverallDistributionRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/latency/overall_distribution', + params: t.type({ + query: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + t.type({ + percentileThreshold: toNumberRt, + }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const setup = await setupRequest(resources); + + const { + environment, + kuery, + serviceName, + transactionType, + transactionName, + start, + end, + percentileThreshold, + } = resources.params.query; + + return getOverallLatencyDistribution({ + environment, + kuery, + serviceName, + transactionType, + transactionName, + start, + end, + percentileThreshold, + setup, + }); + }, +}); + +export const latencyDistributionRouteRepository = + createApmServerRouteRepository().add(latencyOverallDistributionRoute); diff --git a/x-pack/plugins/enterprise_search/server/integrations.ts b/x-pack/plugins/enterprise_search/server/integrations.ts index 48909261243e8..eee5cdc3aaec3 100644 --- a/x-pack/plugins/enterprise_search/server/integrations.ts +++ b/x-pack/plugins/enterprise_search/server/integrations.ts @@ -301,4 +301,67 @@ export const registerEnterpriseSearchIntegrations = ( ...integration, }); }); + + customIntegrations.registerCustomIntegration({ + id: 'app_search_web_crawler', + title: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.webCrawlerName', { + defaultMessage: 'Web Crawler', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.appSearch.integrations.webCrawlerDescription', + { + defaultMessage: "Add search to your website with App Search's web crawler.", + } + ), + categories: ['website_search'], + uiInternalPath: '/app/enterprise_search/app_search/engines/new?method=crawler', + icons: [ + { + type: 'eui', + src: 'globe', + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); + + customIntegrations.registerCustomIntegration({ + id: 'app_search_json', + title: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.jsonName', { + defaultMessage: 'JSON', + }), + description: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.jsonDescription', { + defaultMessage: 'Search over your JSON data with App Search.', + }), + categories: ['upload_file'], + uiInternalPath: '/app/enterprise_search/app_search/engines/new?method=json', + icons: [ + { + type: 'eui', + src: 'exportAction', + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); + + customIntegrations.registerCustomIntegration({ + id: 'app_search_api', + title: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.apiName', { + defaultMessage: 'API', + }), + description: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.apiDescription', { + defaultMessage: "Add search to your application with App Search's robust APIs.", + }), + categories: ['custom'], + uiInternalPath: '/app/enterprise_search/app_search/engines/new?method=api', + icons: [ + { + type: 'eui', + src: 'editorCodeBlock', + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); }; diff --git a/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx b/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx index 8752a4118fb6a..1a44beab260cb 100644 --- a/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx +++ b/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx @@ -50,17 +50,13 @@ const initLegacyShims = () => { ruleTypeRegistry: ruleTypeRegistryMock.create(), }; const data = { query: { timefilter: { timefilter: {} } } } as any; - const ngInjector = {} as angular.auto.IInjectorService; - Legacy.init( - { - core: coreMock.createStart(), - data, - isCloud: false, - triggersActionsUi, - usageCollection: {}, - } as any, - ngInjector - ); + Legacy.init({ + core: coreMock.createStart(), + data, + isCloud: false, + triggersActionsUi, + usageCollection: {}, + } as any); }; const ALERTS_FEATURE_ID = 'alerts'; diff --git a/x-pack/plugins/monitoring/public/angular/angular_i18n/directive.ts b/x-pack/plugins/monitoring/public/angular/angular_i18n/directive.ts deleted file mode 100644 index 1aaff99a6a5c1..0000000000000 --- a/x-pack/plugins/monitoring/public/angular/angular_i18n/directive.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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 { IDirective, IRootElementService, IScope } from 'angular'; - -import { I18nServiceType } from './provider'; - -interface I18nScope extends IScope { - values?: Record; - defaultMessage: string; - id: string; -} - -const HTML_KEY_PREFIX = 'html_'; -const PLACEHOLDER_SEPARATOR = '@I18N@'; - -export const i18nDirective: [string, string, typeof i18nDirectiveFn] = [ - 'i18n', - '$sanitize', - i18nDirectiveFn, -]; - -function i18nDirectiveFn( - i18n: I18nServiceType, - $sanitize: (html: string) => string -): IDirective { - return { - restrict: 'A', - scope: { - id: '@i18nId', - defaultMessage: '@i18nDefaultMessage', - values: ' { - setContent($element, $scope, $sanitize, i18n); - }); - } else { - setContent($element, $scope, $sanitize, i18n); - } - }, - }; -} - -function setContent( - $element: IRootElementService, - $scope: I18nScope, - $sanitize: (html: string) => string, - i18n: I18nServiceType -) { - const originalValues = $scope.values; - const valuesWithPlaceholders = {} as Record; - let hasValuesWithPlaceholders = false; - - // If we have values with the keys that start with HTML_KEY_PREFIX we should replace - // them with special placeholders that later on will be inserted as HTML - // into the DOM, the rest of the content will be treated as text. We don't - // sanitize values at this stage as some of the values can be excluded from - // the translated string (e.g. not used by ICU conditional statements). - if (originalValues) { - for (const [key, value] of Object.entries(originalValues)) { - if (key.startsWith(HTML_KEY_PREFIX)) { - valuesWithPlaceholders[ - key.slice(HTML_KEY_PREFIX.length) - ] = `${PLACEHOLDER_SEPARATOR}${key}${PLACEHOLDER_SEPARATOR}`; - - hasValuesWithPlaceholders = true; - } else { - valuesWithPlaceholders[key] = value; - } - } - } - - const label = i18n($scope.id, { - values: valuesWithPlaceholders, - defaultMessage: $scope.defaultMessage, - }); - - // If there are no placeholders to replace treat everything as text, otherwise - // insert label piece by piece replacing every placeholder with corresponding - // sanitized HTML content. - if (!hasValuesWithPlaceholders) { - $element.text(label); - } else { - $element.empty(); - for (const contentOrPlaceholder of label.split(PLACEHOLDER_SEPARATOR)) { - if (!contentOrPlaceholder) { - continue; - } - - $element.append( - originalValues!.hasOwnProperty(contentOrPlaceholder) - ? $sanitize(originalValues![contentOrPlaceholder]) - : document.createTextNode(contentOrPlaceholder) - ); - } - } -} diff --git a/x-pack/plugins/monitoring/public/angular/angular_i18n/filter.ts b/x-pack/plugins/monitoring/public/angular/angular_i18n/filter.ts deleted file mode 100644 index e4e553fa47b6f..0000000000000 --- a/x-pack/plugins/monitoring/public/angular/angular_i18n/filter.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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 { I18nServiceType } from './provider'; - -export const i18nFilter: [string, typeof i18nFilterFn] = ['i18n', i18nFilterFn]; - -function i18nFilterFn(i18n: I18nServiceType) { - return (id: string, { defaultMessage = '', values = {} } = {}) => { - return i18n(id, { - values, - defaultMessage, - }); - }; -} diff --git a/x-pack/plugins/monitoring/public/angular/angular_i18n/index.ts b/x-pack/plugins/monitoring/public/angular/angular_i18n/index.ts deleted file mode 100644 index 8915c96e59be0..0000000000000 --- a/x-pack/plugins/monitoring/public/angular/angular_i18n/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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. - */ - -export { I18nProvider } from './provider'; - -export { i18nFilter } from './filter'; -export { i18nDirective } from './directive'; - -// re-export types: https://github.com/babel/babel-loader/issues/603 -import { I18nServiceType as _I18nServiceType } from './provider'; -export type I18nServiceType = _I18nServiceType; diff --git a/x-pack/plugins/monitoring/public/angular/angular_i18n/provider.ts b/x-pack/plugins/monitoring/public/angular/angular_i18n/provider.ts deleted file mode 100644 index b1da1bad6e399..0000000000000 --- a/x-pack/plugins/monitoring/public/angular/angular_i18n/provider.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; - -export type I18nServiceType = ReturnType; - -export class I18nProvider implements angular.IServiceProvider { - public addTranslation = i18n.addTranslation; - public getTranslation = i18n.getTranslation; - public setLocale = i18n.setLocale; - public getLocale = i18n.getLocale; - public setDefaultLocale = i18n.setDefaultLocale; - public getDefaultLocale = i18n.getDefaultLocale; - public setFormats = i18n.setFormats; - public getFormats = i18n.getFormats; - public getRegisteredLocales = i18n.getRegisteredLocales; - public init = i18n.init; - public load = i18n.load; - public $get = () => i18n.translate; -} diff --git a/x-pack/plugins/monitoring/public/angular/helpers/format_angular_http_error.ts b/x-pack/plugins/monitoring/public/angular/helpers/format_angular_http_error.ts deleted file mode 100644 index abdcf157a3c86..0000000000000 --- a/x-pack/plugins/monitoring/public/angular/helpers/format_angular_http_error.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; -import type { IHttpResponse } from 'angular'; - -type AngularHttpError = IHttpResponse<{ message: string }>; - -export function isAngularHttpError(error: any): error is AngularHttpError { - return ( - error && - typeof error.status === 'number' && - typeof error.statusText === 'string' && - error.data && - typeof error.data.message === 'string' - ); -} - -export function formatAngularHttpError(error: AngularHttpError) { - // is an Angular $http "error object" - if (error.status === -1) { - // status = -1 indicates that the request was failed to reach the server - return i18n.translate('xpack.monitoring.notify.fatalError.unavailableServerErrorMessage', { - defaultMessage: - 'An HTTP request has failed to connect. ' + - 'Please check if the Kibana server is running and that your browser has a working connection, ' + - 'or contact your system administrator.', - }); - } - - return i18n.translate('xpack.monitoring.notify.fatalError.errorStatusMessage', { - defaultMessage: 'Error {errStatus} {errStatusText}: {errMessage}', - values: { - errStatus: error.status, - errStatusText: error.statusText, - errMessage: error.data.message, - }, - }); -} diff --git a/x-pack/plugins/monitoring/public/angular/top_nav/angular_config.tsx b/x-pack/plugins/monitoring/public/angular/top_nav/angular_config.tsx deleted file mode 100644 index 9c2e931d24a94..0000000000000 --- a/x-pack/plugins/monitoring/public/angular/top_nav/angular_config.tsx +++ /dev/null @@ -1,349 +0,0 @@ -/* - * 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 { - ICompileProvider, - IHttpProvider, - IHttpService, - ILocationProvider, - IModule, - IRootScopeService, - IRequestConfig, -} from 'angular'; -import $ from 'jquery'; -import { set } from '@elastic/safer-lodash-set'; -import { get } from 'lodash'; -import * as Rx from 'rxjs'; -import { ChromeBreadcrumb, EnvironmentMode, PackageInfo } from 'kibana/public'; -import { History } from 'history'; - -import { CoreStart } from 'kibana/public'; -import { formatAngularHttpError, isAngularHttpError } from '../helpers/format_angular_http_error'; - -export interface RouteConfiguration { - controller?: string | ((...args: any[]) => void); - redirectTo?: string; - resolveRedirectTo?: (...args: any[]) => void; - reloadOnSearch?: boolean; - reloadOnUrl?: boolean; - outerAngularWrapperRoute?: boolean; - resolve?: object; - template?: string; - k7Breadcrumbs?: (...args: any[]) => ChromeBreadcrumb[]; - requireUICapability?: string; -} - -function isSystemApiRequest(request: IRequestConfig) { - const { headers } = request; - return headers && !!headers['kbn-system-request']; -} - -/** - * Detects whether a given angular route is a dummy route that doesn't - * require any action. There are two ways this can happen: - * If `outerAngularWrapperRoute` is set on the route config object, - * it means the local application service set up this route on the outer angular - * and the internal routes will handle the hooks. - * - * If angular did not detect a route and it is the local angular, we are currently - * navigating away from a URL controlled by a local angular router and the - * application will get unmounted. In this case the outer router will handle - * the hooks. - * @param $route Injected $route dependency - * @param isLocalAngular Flag whether this is the local angular router - */ -function isDummyRoute($route: any, isLocalAngular: boolean) { - return ( - ($route.current && $route.current.$$route && $route.current.$$route.outerAngularWrapperRoute) || - (!$route.current && isLocalAngular) - ); -} - -export const configureAppAngularModule = ( - angularModule: IModule, - newPlatform: { - core: CoreStart; - readonly env: { - mode: Readonly; - packageInfo: Readonly; - }; - }, - isLocalAngular: boolean, - getHistory?: () => History -) => { - const core = 'core' in newPlatform ? newPlatform.core : newPlatform; - const packageInfo = newPlatform.env.packageInfo; - - angularModule - .value('kbnVersion', packageInfo.version) - .value('buildNum', packageInfo.buildNum) - .value('buildSha', packageInfo.buildSha) - .value('esUrl', getEsUrl(core)) - .value('uiCapabilities', core.application.capabilities) - .config(setupCompileProvider(newPlatform.env.mode.dev)) - .config(setupLocationProvider()) - .config($setupXsrfRequestInterceptor(packageInfo.version)) - .run(capture$httpLoadingCount(core)) - .run(digestOnHashChange(getHistory)) - .run($setupBreadcrumbsAutoClear(core, isLocalAngular)) - .run($setupBadgeAutoClear(core, isLocalAngular)) - .run($setupHelpExtensionAutoClear(core, isLocalAngular)) - .run($setupUICapabilityRedirect(core)); -}; - -const getEsUrl = (newPlatform: CoreStart) => { - const a = document.createElement('a'); - a.href = newPlatform.http.basePath.prepend('/elasticsearch'); - const protocolPort = /https/.test(a.protocol) ? 443 : 80; - const port = a.port || protocolPort; - return { - host: a.hostname, - port, - protocol: a.protocol, - pathname: a.pathname, - }; -}; - -const digestOnHashChange = (getHistory?: () => History) => ($rootScope: IRootScopeService) => { - if (!getHistory) return; - const unlisten = getHistory().listen(() => { - // dispatch synthetic hash change event to update hash history objects and angular routing - // this is necessary because hash updates triggered by using popState won't trigger this event naturally. - // this has to happen in the next tick to not change the existing timing of angular digest cycles. - setTimeout(() => { - window.dispatchEvent(new HashChangeEvent('hashchange')); - }, 0); - }); - $rootScope.$on('$destroy', unlisten); -}; - -const setupCompileProvider = (devMode: boolean) => ($compileProvider: ICompileProvider) => { - if (!devMode) { - $compileProvider.debugInfoEnabled(false); - } -}; - -const setupLocationProvider = () => ($locationProvider: ILocationProvider) => { - $locationProvider.html5Mode({ - enabled: false, - requireBase: false, - rewriteLinks: false, - }); - - $locationProvider.hashPrefix(''); -}; - -export const $setupXsrfRequestInterceptor = (version: string) => { - // Configure jQuery prefilter - $.ajaxPrefilter(({ kbnXsrfToken = true }: any, originalOptions, jqXHR) => { - if (kbnXsrfToken) { - jqXHR.setRequestHeader('kbn-version', version); - } - }); - - return ($httpProvider: IHttpProvider) => { - // Configure $httpProvider interceptor - $httpProvider.interceptors.push(() => { - return { - request(opts) { - const { kbnXsrfToken = true } = opts as any; - if (kbnXsrfToken) { - set(opts, ['headers', 'kbn-version'], version); - } - return opts; - }, - }; - }); - }; -}; - -/** - * Injected into angular module by ui/chrome angular integration - * and adds a root-level watcher that will capture the count of - * active $http requests on each digest loop and expose the count to - * the core.loadingCount api - */ -const capture$httpLoadingCount = - (newPlatform: CoreStart) => ($rootScope: IRootScopeService, $http: IHttpService) => { - newPlatform.http.addLoadingCountSource( - new Rx.Observable((observer) => { - const unwatch = $rootScope.$watch(() => { - const reqs = $http.pendingRequests || []; - observer.next(reqs.filter((req) => !isSystemApiRequest(req)).length); - }); - - return unwatch; - }) - ); - }; - -/** - * integrates with angular to automatically redirect to home if required - * capability is not met - */ -const $setupUICapabilityRedirect = - (newPlatform: CoreStart) => ($rootScope: IRootScopeService, $injector: any) => { - const isKibanaAppRoute = window.location.pathname.endsWith('/app/kibana'); - // this feature only works within kibana app for now after everything is - // switched to the application service, this can be changed to handle all - // apps. - if (!isKibanaAppRoute) { - return; - } - $rootScope.$on( - '$routeChangeStart', - (event, { $$route: route }: { $$route?: RouteConfiguration } = {}) => { - if (!route || !route.requireUICapability) { - return; - } - - if (!get(newPlatform.application.capabilities, route.requireUICapability)) { - $injector.get('$location').url('/home'); - event.preventDefault(); - } - } - ); - }; - -/** - * internal angular run function that will be called when angular bootstraps and - * lets us integrate with the angular router so that we can automatically clear - * the breadcrumbs if we switch to a Kibana app that does not use breadcrumbs correctly - */ -const $setupBreadcrumbsAutoClear = - (newPlatform: CoreStart, isLocalAngular: boolean) => - ($rootScope: IRootScopeService, $injector: any) => { - // A flag used to determine if we should automatically - // clear the breadcrumbs between angular route changes. - let breadcrumbSetSinceRouteChange = false; - const $route = $injector.has('$route') ? $injector.get('$route') : {}; - - // reset breadcrumbSetSinceRouteChange any time the breadcrumbs change, even - // if it was done directly through the new platform - newPlatform.chrome.getBreadcrumbs$().subscribe({ - next() { - breadcrumbSetSinceRouteChange = true; - }, - }); - - $rootScope.$on('$routeChangeStart', () => { - breadcrumbSetSinceRouteChange = false; - }); - - $rootScope.$on('$routeChangeSuccess', () => { - if (isDummyRoute($route, isLocalAngular)) { - return; - } - const current = $route.current || {}; - - if (breadcrumbSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) { - return; - } - - const k7BreadcrumbsProvider = current.k7Breadcrumbs; - if (!k7BreadcrumbsProvider) { - newPlatform.chrome.setBreadcrumbs([]); - return; - } - - try { - newPlatform.chrome.setBreadcrumbs($injector.invoke(k7BreadcrumbsProvider)); - } catch (error) { - if (isAngularHttpError(error)) { - error = formatAngularHttpError(error); - } - newPlatform.fatalErrors.add(error, 'location'); - } - }); - }; - -/** - * internal angular run function that will be called when angular bootstraps and - * lets us integrate with the angular router so that we can automatically clear - * the badge if we switch to a Kibana app that does not use the badge correctly - */ -const $setupBadgeAutoClear = - (newPlatform: CoreStart, isLocalAngular: boolean) => - ($rootScope: IRootScopeService, $injector: any) => { - // A flag used to determine if we should automatically - // clear the badge between angular route changes. - let badgeSetSinceRouteChange = false; - const $route = $injector.has('$route') ? $injector.get('$route') : {}; - - $rootScope.$on('$routeChangeStart', () => { - badgeSetSinceRouteChange = false; - }); - - $rootScope.$on('$routeChangeSuccess', () => { - if (isDummyRoute($route, isLocalAngular)) { - return; - } - const current = $route.current || {}; - - if (badgeSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) { - return; - } - - const badgeProvider = current.badge; - if (!badgeProvider) { - newPlatform.chrome.setBadge(undefined); - return; - } - - try { - newPlatform.chrome.setBadge($injector.invoke(badgeProvider)); - } catch (error) { - if (isAngularHttpError(error)) { - error = formatAngularHttpError(error); - } - newPlatform.fatalErrors.add(error, 'location'); - } - }); - }; - -/** - * internal angular run function that will be called when angular bootstraps and - * lets us integrate with the angular router so that we can automatically clear - * the helpExtension if we switch to a Kibana app that does not set its own - * helpExtension - */ -const $setupHelpExtensionAutoClear = - (newPlatform: CoreStart, isLocalAngular: boolean) => - ($rootScope: IRootScopeService, $injector: any) => { - /** - * reset helpExtensionSetSinceRouteChange any time the helpExtension changes, even - * if it was done directly through the new platform - */ - let helpExtensionSetSinceRouteChange = false; - newPlatform.chrome.getHelpExtension$().subscribe({ - next() { - helpExtensionSetSinceRouteChange = true; - }, - }); - - const $route = $injector.has('$route') ? $injector.get('$route') : {}; - - $rootScope.$on('$routeChangeStart', () => { - if (isDummyRoute($route, isLocalAngular)) { - return; - } - helpExtensionSetSinceRouteChange = false; - }); - - $rootScope.$on('$routeChangeSuccess', () => { - if (isDummyRoute($route, isLocalAngular)) { - return; - } - const current = $route.current || {}; - - if (helpExtensionSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) { - return; - } - - newPlatform.chrome.setHelpExtension(current.helpExtension); - }); - }; diff --git a/x-pack/plugins/monitoring/public/angular/top_nav/index.ts b/x-pack/plugins/monitoring/public/angular/top_nav/index.ts deleted file mode 100644 index b3501e4cbad1f..0000000000000 --- a/x-pack/plugins/monitoring/public/angular/top_nav/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * 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. - */ - -export * from './angular_config'; -// @ts-ignore -export { createTopNavDirective, createTopNavHelper, loadKbnTopNavDirectives } from './kbn_top_nav'; diff --git a/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.d.ts b/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.d.ts deleted file mode 100644 index 0cff77241bb9c..0000000000000 --- a/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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 { Injectable, IDirectiveFactory, IScope, IAttributes, IController } from 'angular'; - -export const createTopNavDirective: Injectable< - IDirectiveFactory ->; -export const createTopNavHelper: ( - options: unknown -) => Injectable>; -export function loadKbnTopNavDirectives(navUi: unknown): void; diff --git a/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.js b/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.js deleted file mode 100644 index 6edcca6aa714a..0000000000000 --- a/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.js +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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 angular from 'angular'; -import 'ngreact'; - -export function createTopNavDirective() { - return { - restrict: 'E', - template: '', - compile: (elem) => { - const child = document.createElement('kbn-top-nav-helper'); - - // Copy attributes to the child directive - for (const attr of elem[0].attributes) { - child.setAttribute(attr.name, attr.value); - } - - // Add a special attribute that will change every time that one - // of the config array's disableButton function return value changes. - child.setAttribute('disabled-buttons', 'disabledButtons'); - - // Append helper directive - elem.append(child); - - const linkFn = ($scope, _, $attr) => { - // Watch config changes - $scope.$watch( - () => { - const config = $scope.$eval($attr.config) || []; - return config.map((item) => { - // Copy key into id, as it's a reserved react propery. - // This is done for Angular directive backward compatibility. - // In React only id is recognized. - if (item.key && !item.id) { - item.id = item.key; - } - - // Watch the disableButton functions - if (typeof item.disableButton === 'function') { - return item.disableButton(); - } - return item.disableButton; - }); - }, - (newVal) => { - $scope.disabledButtons = newVal; - }, - true - ); - }; - - return linkFn; - }, - }; -} - -export const createTopNavHelper = - ({ TopNavMenu }) => - (reactDirective) => { - return reactDirective(TopNavMenu, [ - ['config', { watchDepth: 'value' }], - ['setMenuMountPoint', { watchDepth: 'reference' }], - ['disabledButtons', { watchDepth: 'reference' }], - - ['query', { watchDepth: 'reference' }], - ['savedQuery', { watchDepth: 'reference' }], - ['intl', { watchDepth: 'reference' }], - - ['onQuerySubmit', { watchDepth: 'reference' }], - ['onFiltersUpdated', { watchDepth: 'reference' }], - ['onRefreshChange', { watchDepth: 'reference' }], - ['onClearSavedQuery', { watchDepth: 'reference' }], - ['onSaved', { watchDepth: 'reference' }], - ['onSavedQueryUpdated', { watchDepth: 'reference' }], - ['onSavedQueryIdChange', { watchDepth: 'reference' }], - - ['indexPatterns', { watchDepth: 'collection' }], - ['filters', { watchDepth: 'collection' }], - - // All modifiers default to true. - // Set to false to hide subcomponents. - 'showSearchBar', - 'showQueryBar', - 'showQueryInput', - 'showSaveQuery', - 'showDatePicker', - 'showFilterBar', - - 'appName', - 'screenTitle', - 'dateRangeFrom', - 'dateRangeTo', - 'savedQueryId', - 'isRefreshPaused', - 'refreshInterval', - 'disableAutoFocus', - 'showAutoRefreshOnly', - - // temporary flag to use the stateful components - 'useDefaultBehaviors', - ]); - }; - -let isLoaded = false; - -export function loadKbnTopNavDirectives(navUi) { - if (!isLoaded) { - isLoaded = true; - angular - .module('kibana') - .directive('kbnTopNav', createTopNavDirective) - .directive('kbnTopNavHelper', createTopNavHelper(navUi)); - } -} diff --git a/x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx b/x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx index e6638b4c4fede..cc8619dbc7ad2 100644 --- a/x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx +++ b/x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx @@ -32,31 +32,8 @@ export const GlobalStateProvider: React.FC = ({ toasts, children, }) => { - // TODO: remove fakeAngularRootScope and fakeAngularLocation when angular is removed - const fakeAngularRootScope: Partial = { - $on: - (name: string, listener: (event: ng.IAngularEvent, ...args: any[]) => any): (() => void) => - () => {}, - $applyAsync: () => {}, - }; - - const fakeAngularLocation: Partial = { - search: () => { - return {} as any; - }, - replace: () => { - return {} as any; - }, - }; - const localState: State = {}; - const state = new GlobalState( - query, - toasts, - fakeAngularRootScope, - fakeAngularLocation, - localState as { [key: string]: unknown } - ); + const state = new GlobalState(query, toasts, localState as { [key: string]: unknown }); const initialState: any = state.getState(); for (const key in initialState) { diff --git a/x-pack/plugins/monitoring/public/legacy_shims.ts b/x-pack/plugins/monitoring/public/legacy_shims.ts index 72d50aac1dbb8..48484421839bd 100644 --- a/x-pack/plugins/monitoring/public/legacy_shims.ts +++ b/x-pack/plugins/monitoring/public/legacy_shims.ts @@ -44,7 +44,7 @@ const angularNoop = () => { export interface IShims { toastNotifications: CoreStart['notifications']['toasts']; capabilities: CoreStart['application']['capabilities']; - getAngularInjector: typeof angularNoop | (() => angular.auto.IInjectorService); + getAngularInjector: typeof angularNoop; getBasePath: () => string; getInjected: (name: string, defaultValue?: unknown) => unknown; breadcrumbs: { @@ -73,23 +73,18 @@ export interface IShims { export class Legacy { private static _shims: IShims; - public static init( - { - core, - data, - isCloud, - triggersActionsUi, - usageCollection, - appMountParameters, - }: MonitoringStartPluginDependencies, - ngInjector?: angular.auto.IInjectorService - ) { + public static init({ + core, + data, + isCloud, + triggersActionsUi, + usageCollection, + appMountParameters, + }: MonitoringStartPluginDependencies) { this._shims = { toastNotifications: core.notifications.toasts, capabilities: core.application.capabilities, - getAngularInjector: ngInjector - ? (): angular.auto.IInjectorService => ngInjector - : angularNoop, + getAngularInjector: angularNoop, getBasePath: (): string => core.http.basePath.get(), getInjected: (name: string, defaultValue?: unknown): string | unknown => core.injectedMetadata.getInjectedVar(name, defaultValue), diff --git a/x-pack/plugins/monitoring/public/url_state.ts b/x-pack/plugins/monitoring/public/url_state.ts index 25086411c65a3..8f89df732b800 100644 --- a/x-pack/plugins/monitoring/public/url_state.ts +++ b/x-pack/plugins/monitoring/public/url_state.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { Subscription } from 'rxjs'; import { History, createHashHistory } from 'history'; import { MonitoringStartPluginDependencies } from './types'; @@ -27,10 +26,6 @@ import { withNotifyOnErrors, } from '../../../../src/plugins/kibana_utils/public'; -interface Route { - params: { _g: unknown }; -} - interface RawObject { [key: string]: unknown; } @@ -57,7 +52,6 @@ export interface MonitoringAppStateTransitions { const GLOBAL_STATE_KEY = '_g'; const objectEquals = (objA: any, objB: any) => JSON.stringify(objA) === JSON.stringify(objB); -// TODO: clean all angular references after angular is removed export class GlobalState { private readonly stateSyncRef: ISyncStateRef; private readonly stateContainer: StateContainer< @@ -70,13 +64,10 @@ export class GlobalState { private readonly timefilterRef: MonitoringStartPluginDependencies['data']['query']['timefilter']['timefilter']; private lastAssignedState: MonitoringAppState = {}; - private lastKnownGlobalState?: string; constructor( queryService: MonitoringStartPluginDependencies['data']['query'], toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts'], - rootScope: Partial, - ngLocation: Partial, externalState: RawObject ) { this.timefilterRef = queryService.timefilter.timefilter; @@ -102,9 +93,6 @@ export class GlobalState { this.stateContainerChangeSub = this.stateContainer.state$.subscribe(() => { this.lastAssignedState = this.getState(); - if (!this.stateContainer.get() && this.lastKnownGlobalState) { - ngLocation.search?.(`${GLOBAL_STATE_KEY}=${this.lastKnownGlobalState}`).replace(); - } // TODO: check if this is not needed after https://github.com/elastic/kibana/pull/109132 is merged if (Legacy.isInitializated()) { @@ -112,15 +100,11 @@ export class GlobalState { } this.syncExternalState(externalState); - rootScope.$applyAsync?.(); }); this.syncQueryStateWithUrlManager = syncQueryStateWithUrl(queryService, this.stateStorage); this.stateSyncRef.start(); - this.startHashSync(rootScope, ngLocation); this.lastAssignedState = this.getState(); - - rootScope.$on?.('$destroy', () => this.destroy()); } private syncExternalState(externalState: { [key: string]: unknown }) { @@ -137,24 +121,6 @@ export class GlobalState { } } - private startHashSync( - rootScope: Partial, - ngLocation: Partial - ) { - rootScope.$on?.( - '$routeChangeStart', - (_: { preventDefault: () => void }, newState: Route, oldState: Route) => { - const currentGlobalState = oldState?.params?._g; - const nextGlobalState = newState?.params?._g; - if (!nextGlobalState && currentGlobalState && typeof currentGlobalState === 'string') { - newState.params._g = currentGlobalState; - ngLocation.search?.(`${GLOBAL_STATE_KEY}=${currentGlobalState}`).replace(); - } - this.lastKnownGlobalState = (nextGlobalState || currentGlobalState) as string; - } - ); - } - public setState(state?: { [key: string]: unknown }) { const currentAppState = this.getState(); const newAppState = { ...currentAppState, ...state }; diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx index af91624769e6b..d852d6fdb9a31 100644 --- a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx +++ b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx @@ -16,9 +16,9 @@ export function SyntheticsAddData() { return ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index e4473b183d729..c12e67bc9b1ae 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -49,7 +49,9 @@ import { MONITORS_DURATION_LABEL, PAGE_LOAD_TIME_LABEL, LABELS_FIELD, + STEP_NAME_LABEL, } from './labels'; +import { SYNTHETICS_STEP_NAME } from './field_names/synthetics'; export const DEFAULT_TIME = { from: 'now-1h', to: 'now' }; @@ -77,6 +79,7 @@ export const FieldLabels: Record = { 'monitor.id': MONITOR_ID_LABEL, 'monitor.status': MONITOR_STATUS_LABEL, 'monitor.duration.us': MONITORS_DURATION_LABEL, + [SYNTHETICS_STEP_NAME]: STEP_NAME_LABEL, 'agent.hostname': AGENT_HOST_LABEL, 'host.hostname': HOST_NAME_LABEL, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts index eff73d242de75..0f28648552728 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts @@ -11,3 +11,5 @@ export const SYNTHETICS_LCP = 'browser.experience.lcp.us'; export const SYNTHETICS_FCP = 'browser.experience.fcp.us'; export const SYNTHETICS_DOCUMENT_ONLOAD = 'browser.experience.load.us'; export const SYNTHETICS_DCL = 'browser.experience.dcl.us'; +export const SYNTHETICS_STEP_NAME = 'synthetics.step.name.keyword'; +export const SYNTHETICS_STEP_DURATION = 'synthetics.step.duration.us'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts index cdaa89fc71389..599f846af2ff9 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts @@ -231,6 +231,20 @@ export const MONITORS_DURATION_LABEL = i18n.translate( } ); +export const STEP_DURATION_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.stepDurationLabel', + { + defaultMessage: 'Step duration', + } +); + +export const STEP_NAME_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.stepNameLabel', + { + defaultMessage: 'Step name', + } +); + export const WEB_APPLICATION_LABEL = i18n.translate( 'xpack.observability.expView.fieldLabels.webApplication', { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 38c9ecc06491d..a31bef7c9c214 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -194,10 +194,12 @@ export class LensAttributes { label, sourceField, columnType, + columnFilter, operationType, }: { sourceField: string; columnType?: string; + columnFilter?: ColumnFilter; operationType?: string; label?: string; seriesConfig: SeriesConfig; @@ -214,6 +216,7 @@ export class LensAttributes { operationType, label, seriesConfig, + columnFilter, }); } if (operationType?.includes('th')) { @@ -228,11 +231,13 @@ export class LensAttributes { label, seriesConfig, operationType, + columnFilter, }: { sourceField: string; operationType: 'average' | 'median' | 'sum' | 'unique_count'; label?: string; seriesConfig: SeriesConfig; + columnFilter?: ColumnFilter; }): | AvgIndexPatternColumn | MedianIndexPatternColumn @@ -247,6 +252,7 @@ export class LensAttributes { operationType: capitalize(operationType), }, }), + filter: columnFilter, operationType, }; } @@ -391,6 +397,7 @@ export class LensAttributes { return this.getNumberColumn({ sourceField: fieldName, columnType, + columnFilter: columnFilters?.[0], operationType, label: columnLabel || label, seriesConfig: layerConfig.seriesConfig, @@ -447,10 +454,10 @@ export class LensAttributes { return this.getColumnBasedOnType({ sourceField, - operationType: breakdown === PERCENTILE ? PERCENTILE_RANKS[0] : operationType, label, layerConfig, colIndex: 0, + operationType: breakdown === PERCENTILE ? PERCENTILE_RANKS[0] : operationType, }); } @@ -629,7 +636,12 @@ export class LensAttributes { [`y-axis-column-${layerId}`]: { ...mainYAxis, label, - filter: { query: columnFilter, language: 'kuery' }, + filter: { + query: mainYAxis.filter + ? `${columnFilter} and ${mainYAxis.filter.query}` + : columnFilter, + language: 'kuery', + }, ...(timeShift ? { timeShift } : {}), }, ...(breakdown && sourceField !== USE_BREAK_DOWN_COLUMN && breakdown !== PERCENTILE diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts index da90f45d15201..fb44da8e4327f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts @@ -29,6 +29,7 @@ import { SYNTHETICS_FCP, SYNTHETICS_LCP, } from '../constants/field_names/synthetics'; +import { buildExistsFilter } from '../utils'; export function getSyntheticsDistributionConfig({ series, @@ -58,7 +59,10 @@ export function getSyntheticsDistributionConfig({ 'url.port', ], baseFilters: [], - definitionFields: ['monitor.name', 'url.full'], + definitionFields: [ + { field: 'monitor.name', nested: 'synthetics.step.name.keyword', singleSelection: true }, + { field: 'url.full', filters: buildExistsFilter('summary.up', indexPattern) }, + ], metricOptions: [ { label: MONITORS_DURATION_LABEL, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts index 5f8a6a28ca81d..f3e3fe0817845 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts @@ -11,6 +11,7 @@ import { SYNTHETICS_DOCUMENT_ONLOAD, SYNTHETICS_FCP, SYNTHETICS_LCP, + SYNTHETICS_STEP_DURATION, } from '../constants/field_names/synthetics'; export const syntheticsFieldFormats: FieldFormat[] = [ @@ -27,6 +28,19 @@ export const syntheticsFieldFormats: FieldFormat[] = [ }, }, }, + { + field: SYNTHETICS_STEP_DURATION, + format: { + id: 'duration', + params: { + inputFormat: 'microseconds', + outputFormat: 'humanizePrecise', + outputPrecision: 1, + showSuffix: true, + useShortSuffix: true, + }, + }, + }, { field: SYNTHETICS_LCP, format: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts index 6df9cdcd0503a..8951ffcda63d8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConfigProps, SeriesConfig } from '../../types'; +import { ColumnFilter, ConfigProps, SeriesConfig } from '../../types'; import { FieldLabels, OPERATION_COLUMN, @@ -21,6 +21,7 @@ import { FCP_LABEL, LCP_LABEL, MONITORS_DURATION_LABEL, + STEP_DURATION_LABEL, UP_LABEL, } from '../constants/labels'; import { @@ -30,10 +31,26 @@ import { SYNTHETICS_DOCUMENT_ONLOAD, SYNTHETICS_FCP, SYNTHETICS_LCP, + SYNTHETICS_STEP_DURATION, + SYNTHETICS_STEP_NAME, } from '../constants/field_names/synthetics'; +import { buildExistsFilter } from '../utils'; const SUMMARY_UP = 'summary.up'; const SUMMARY_DOWN = 'summary.down'; +export const isStepLevelMetric = (metric?: string) => { + if (!metric) { + return false; + } + return [ + SYNTHETICS_LCP, + SYNTHETICS_FCP, + SYNTHETICS_CLS, + SYNTHETICS_DCL, + SYNTHETICS_STEP_DURATION, + SYNTHETICS_DOCUMENT_ONLOAD, + ].includes(metric); +}; export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { reportType: ReportTypes.KPI, @@ -50,10 +67,19 @@ export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesCon ], hasOperationType: false, filterFields: ['observer.geo.name', 'monitor.type', 'tags'], - breakdownFields: ['observer.geo.name', 'monitor.type', 'monitor.name', PERCENTILE], + breakdownFields: [ + 'observer.geo.name', + 'monitor.type', + 'monitor.name', + SYNTHETICS_STEP_NAME, + PERCENTILE, + ], baseFilters: [], palette: { type: 'palette', name: 'status' }, - definitionFields: ['monitor.name', 'url.full'], + definitionFields: [ + { field: 'monitor.name', nested: SYNTHETICS_STEP_NAME, singleSelection: true }, + { field: 'url.full', filters: buildExistsFilter('summary.up', indexPattern) }, + ], metricOptions: [ { label: MONITORS_DURATION_LABEL, @@ -73,37 +99,59 @@ export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesCon label: DOWN_LABEL, columnType: OPERATION_COLUMN, }, + { + label: STEP_DURATION_LABEL, + field: SYNTHETICS_STEP_DURATION, + id: SYNTHETICS_STEP_DURATION, + columnType: OPERATION_COLUMN, + columnFilters: [STEP_END_FILTER], + }, { label: LCP_LABEL, field: SYNTHETICS_LCP, id: SYNTHETICS_LCP, columnType: OPERATION_COLUMN, + columnFilters: [STEP_METRIC_FILTER], }, { label: FCP_LABEL, field: SYNTHETICS_FCP, id: SYNTHETICS_FCP, columnType: OPERATION_COLUMN, + columnFilters: [STEP_METRIC_FILTER], }, { label: DCL_LABEL, field: SYNTHETICS_DCL, id: SYNTHETICS_DCL, columnType: OPERATION_COLUMN, + columnFilters: [STEP_METRIC_FILTER], }, { label: DOCUMENT_ONLOAD_LABEL, field: SYNTHETICS_DOCUMENT_ONLOAD, id: SYNTHETICS_DOCUMENT_ONLOAD, columnType: OPERATION_COLUMN, + columnFilters: [STEP_METRIC_FILTER], }, { label: CLS_LABEL, field: SYNTHETICS_CLS, id: SYNTHETICS_CLS, columnType: OPERATION_COLUMN, + columnFilters: [STEP_METRIC_FILTER], }, ], labels: { ...FieldLabels, [SUMMARY_UP]: UP_LABEL, [SUMMARY_DOWN]: DOWN_LABEL }, }; } + +const STEP_METRIC_FILTER: ColumnFilter = { + language: 'kuery', + query: `synthetics.type: step/metrics`, +}; + +const STEP_END_FILTER: ColumnFilter = { + language: 'kuery', + query: `synthetics.type: step/end`, +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts index adc6d4bb14462..4563509eeb19a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts @@ -77,7 +77,8 @@ export const sampleAttributeCoreWebVital = { dataType: 'number', filter: { language: 'kuery', - query: 'transaction.type: page-load and processor.event: transaction', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.marks.agent.largestContentfulPaint < 2500', }, isBucketed: false, label: 'Good', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx index a235cbd8852ad..f30a80f87ebb7 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import styled from 'styled-components'; import { EuiSuperSelect, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -17,6 +17,8 @@ import { PERCENTILE, } from '../../configurations/constants'; import { SeriesConfig, SeriesUrl } from '../../types'; +import { SYNTHETICS_STEP_NAME } from '../../configurations/constants/field_names/synthetics'; +import { isStepLevelMetric } from '../../configurations/synthetics/kpi_over_time_config'; interface Props { seriesId: number; @@ -51,6 +53,18 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) { } }; + useEffect(() => { + if ( + !isStepLevelMetric(series.selectedMetricField) && + selectedBreakdown === SYNTHETICS_STEP_NAME + ) { + setSeries(seriesId, { + ...series, + breakdown: undefined, + }); + } + }); + if (!seriesConfig) { return null; } @@ -71,11 +85,26 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) { } const options = items - .map(({ id, label }) => ({ - inputDisplay: label, - value: id, - dropdownDisplay: label, - })) + .map(({ id, label }) => { + if (id === SYNTHETICS_STEP_NAME && !isStepLevelMetric(series.selectedMetricField)) { + return { + inputDisplay: label, + value: id, + dropdownDisplay: ( + + <>{label} + + ), + disabled: true, + }; + } else { + return { + inputDisplay: label, + value: id, + dropdownDisplay: label, + }; + } + }) .filter(({ value }) => !(value === PERCENTILE && isRecordsMetric)); let valueOfSelected = @@ -121,6 +150,14 @@ export const BREAKDOWN_WARNING = i18n.translate('xpack.observability.exp.breakDo defaultMessage: 'Breakdowns can be applied to only one series at a time.', }); +export const BREAKDOWN_UNAVAILABLE = i18n.translate( + 'xpack.observability.exp.breakDownFilter.unavailable', + { + defaultMessage: + 'Step name breakdown is not available for monitor duration metric. Use step duration metric to breakdown by step name.', + } +); + const Wrapper = styled.span` .euiToolTipAnchor { width: 100%; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx index 4e1c385921908..8e64f4bcea680 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx @@ -31,7 +31,14 @@ export function IncompleteBadge({ seriesConfig, series }: Props) { const incompleteDefinition = isEmpty(reportDefinitions) ? i18n.translate('xpack.observability.overview.exploratoryView.missingReportDefinition', { defaultMessage: 'Missing {reportDefinition}', - values: { reportDefinition: labels?.[definitionFields[0]] }, + values: { + reportDefinition: + labels?.[ + typeof definitionFields[0] === 'string' + ? definitionFields[0] + : definitionFields[0].field + ], + }, }) : ''; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx index fbd7c34303d94..a665ec1999133 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx @@ -6,10 +6,13 @@ */ import React from 'react'; +import { isEmpty } from 'lodash'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { SeriesConfig, SeriesUrl } from '../../types'; import { ReportDefinitionField } from './report_definition_field'; +import { isStepLevelMetric } from '../../configurations/synthetics/kpi_over_time_config'; +import { SYNTHETICS_STEP_NAME } from '../../configurations/constants/field_names/synthetics'; export function ReportDefinitionCol({ seriesId, @@ -41,19 +44,64 @@ export function ReportDefinitionCol({ } }; + const hasFieldDataSelected = (field: string) => { + return !isEmpty(series.reportDefinitions?.[field]); + }; + return ( - {definitionFields.map((field) => ( - - - - ))} + {definitionFields.map((field) => { + const fieldStr = typeof field === 'string' ? field : field.field; + const singleSelection = typeof field !== 'string' && field.singleSelection; + const nestedField = typeof field !== 'string' && field.nested; + const filters = typeof field !== 'string' ? field.filters : undefined; + + const isNonStepMetric = !isStepLevelMetric(series.selectedMetricField); + + const hideNestedStep = nestedField === SYNTHETICS_STEP_NAME && isNonStepMetric; + + if (hideNestedStep && nestedField && selectedReportDefinitions[nestedField]?.length > 0) { + setSeries(seriesId, { + ...series, + reportDefinitions: { ...selectedReportDefinitions, [nestedField]: [] }, + }); + } + + let nestedFieldElement; + + if (nestedField && hasFieldDataSelected(fieldStr) && !hideNestedStep) { + nestedFieldElement = ( + + + + ); + } + + return ( + <> + + + + {nestedFieldElement} + + ); + })} ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx index 01f36e85c03ae..f3e0eb767d336 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { isEmpty } from 'lodash'; -import { ExistsFilter } from '@kbn/es-query'; +import { ExistsFilter, PhraseFilter } from '@kbn/es-query'; import FieldValueSuggestions from '../../../field_value_suggestions'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch'; @@ -19,32 +19,49 @@ import { ALL_VALUES_SELECTED } from '../../../field_value_suggestions/field_valu interface Props { seriesId: number; series: SeriesUrl; - field: string; + singleSelection?: boolean; + keepHistory?: boolean; + field: string | { field: string; nested: string }; seriesConfig: SeriesConfig; onChange: (field: string, value?: string[]) => void; + filters?: Array; } -export function ReportDefinitionField({ series, field, seriesConfig, onChange }: Props) { +export function ReportDefinitionField({ + singleSelection, + keepHistory, + series, + field: fieldProp, + seriesConfig, + onChange, + filters, +}: Props) { const { indexPattern } = useAppIndexPatternContext(series.dataType); + const field = typeof fieldProp === 'string' ? fieldProp : fieldProp.field; + const { reportDefinitions: selectedReportDefinitions = {} } = series; const { labels, baseFilters, definitionFields } = seriesConfig; const queryFilters = useMemo(() => { const filtersN: ESFilter[] = []; - (baseFilters ?? []).forEach((qFilter: PersistableFilter | ExistsFilter) => { - if (qFilter.query) { - filtersN.push(qFilter.query); - } - const existFilter = qFilter as ExistsFilter; - if (existFilter.query.exists) { - filtersN.push({ exists: existFilter.query.exists }); - } - }); + (baseFilters ?? []) + .concat(filters ?? []) + .forEach((qFilter: PersistableFilter | ExistsFilter) => { + if (qFilter.query) { + filtersN.push(qFilter.query); + } + const existFilter = qFilter as ExistsFilter; + if (existFilter.query.exists) { + filtersN.push({ exists: existFilter.query.exists }); + } + }); if (!isEmpty(selectedReportDefinitions)) { - definitionFields.forEach((fieldT) => { + definitionFields.forEach((fieldObj) => { + const fieldT = typeof fieldObj === 'string' ? fieldObj : fieldObj.field; + if (indexPattern && selectedReportDefinitions?.[fieldT] && fieldT !== field) { const values = selectedReportDefinitions?.[fieldT]; if (!values.includes(ALL_VALUES_SELECTED)) { @@ -65,7 +82,7 @@ export function ReportDefinitionField({ series, field, seriesConfig, onChange }: return ( ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx index 180be1ac0414f..12e0ceca20649 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx @@ -48,13 +48,13 @@ export function ExpandedSeriesRow(seriesProps: Props) { return (
- - + + - + diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 001664cf12783..acd49fc25588e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -56,7 +56,15 @@ export interface SeriesConfig { filterFields: Array; seriesTypes: SeriesType[]; baseFilters?: Array; - definitionFields: string[]; + definitionFields: Array< + | string + | { + field: string; + nested?: string; + singleSelection?: boolean; + filters?: Array; + } + >; metricOptions?: MetricOption[]; labels: Record; hasOperationType: boolean; diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx index 0735df53888aa..e04d5463d5494 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx @@ -42,6 +42,7 @@ export function FieldValueCombobox({ usePrependLabel = true, compressed = true, required = true, + singleSelection = false, allowAllValuesSelection, onChange: onSelectionChange, }: FieldValueSelectionProps) { @@ -68,6 +69,7 @@ export function FieldValueCombobox({ const comboBox = ( ); diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts index 6f6d520a83154..95b24aa69b1e7 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts @@ -29,6 +29,7 @@ interface CommonProps { allowAllValuesSelection?: boolean; cardinalityField?: string; required?: boolean; + keepHistory?: boolean; } export type FieldValueSuggestionsProps = CommonProps & { diff --git a/x-pack/plugins/observability/public/pages/overview/empty_section.ts b/x-pack/plugins/observability/public/pages/overview/empty_section.ts index 2747b2ecdebc9..f249a820a60a4 100644 --- a/x-pack/plugins/observability/public/pages/overview/empty_section.ts +++ b/x-pack/plugins/observability/public/pages/overview/empty_section.ts @@ -69,7 +69,7 @@ export const getEmptySections = ({ core }: { core: CoreStart }): ISection[] => { linkTitle: i18n.translate('xpack.observability.emptySection.apps.uptime.link', { defaultMessage: 'Install Heartbeat', }), - href: core.http.basePath.prepend('/app/home#/tutorial/uptimeMonitors'), + href: core.http.basePath.prepend('/app/integrations/detail/synthetics/overview'), }, { id: 'ux', diff --git a/x-pack/plugins/uptime/public/apps/use_no_data_config.ts b/x-pack/plugins/uptime/public/apps/use_no_data_config.ts index dc00a25e3a111..6e73a6d5e8268 100644 --- a/x-pack/plugins/uptime/public/apps/use_no_data_config.ts +++ b/x-pack/plugins/uptime/public/apps/use_no_data_config.ts @@ -31,13 +31,13 @@ export function useNoDataConfig(): KibanaPageTemplateProps['noDataConfig'] { actions: { beats: { title: i18n.translate('xpack.uptime.noDataConfig.beatsCard.title', { - defaultMessage: 'Add monitors with Heartbeat', + defaultMessage: 'Add monitors with the Elastic Synthetics integration', }), description: i18n.translate('xpack.uptime.noDataConfig.beatsCard.description', { defaultMessage: 'Proactively monitor the availability of your sites and services. Receive alerts and resolve issues faster to optimize your users experience.', }), - href: basePath + `/app/home#/tutorial/uptimeMonitors`, + href: basePath + `/app/integrations/detail/synthetics/overview`, }, }, docsLink: docLinks!.links.observability.guide, diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx index 0265588c3fdeb..76b9378ca4ff6 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx @@ -45,11 +45,13 @@ describe('ActionMenuContent', () => { it('renders Add Data link', () => { const { getByLabelText, getByText } = render(); - const addDataAnchor = getByLabelText('Navigate to a tutorial about adding Uptime data'); + const addDataAnchor = getByLabelText( + 'Navigate to the Elastic Synthetics integration to add Uptime data' + ); // this href value is mocked, so it doesn't correspond to the real link // that Kibana core services will provide - expect(addDataAnchor.getAttribute('href')).toBe('/home#/tutorial/uptimeMonitors'); + expect(addDataAnchor.getAttribute('href')).toBe('/integrations/detail/synthetics/overview'); expect(getByText('Add data')); }); }); diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index 21ef3428696e9..789953258750b 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -56,9 +56,8 @@ export function ActionMenuContent(): React.ReactElement { time: { from: dateRangeStart, to: dateRangeEnd }, breakdown: monitorId ? 'observer.geo.name' : 'monitor.type', reportDefinitions: { - 'monitor.name': selectedMonitor?.monitor?.name - ? [selectedMonitor?.monitor?.name] - : ['ALL_VALUES'], + 'monitor.name': selectedMonitor?.monitor?.name ? [selectedMonitor?.monitor?.name] : [], + 'url.full': ['ALL_VALUES'], }, name: monitorId ? `${monitorId}-response-duration` : 'All monitors response duration', }, @@ -100,9 +99,11 @@ export function ActionMenuContent(): React.ReactElement { diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index ac129bdb327d9..60ccec84c3bb1 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -83,7 +83,7 @@ const createMockStore = () => { const mockAppUrls: Record = { uptime: '/app/uptime', observability: '/app/observability', - '/home#/tutorial/uptimeMonitors': '/home#/tutorial/uptimeMonitors', + '/integrations/detail/synthetics/overview': '/integrations/detail/synthetics/overview', }; /* default mock core */ diff --git a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts index 6e2025a7fa2ca..3388d5b4aa379 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; import { IKibanaSearchRequest } from '../../../../../src/plugins/data/common'; import type { FailedTransactionsCorrelationsParams } from '../../../../plugins/apm/common/search_strategies/failed_transactions_correlations/types'; -import type { SearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types'; +import type { RawSearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types'; import { APM_SEARCH_STRATEGIES } from '../../../../plugins/apm/common/search_strategies/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -23,7 +23,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const getRequestBody = () => { const request: IKibanaSearchRequest< - FailedTransactionsCorrelationsParams & SearchStrategyClientParams + FailedTransactionsCorrelationsParams & RawSearchStrategyClientParams > = { params: { environment: 'ENVIRONMENT_ALL', diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency.ts b/x-pack/test/apm_api_integration/tests/correlations/latency.ts index 99aee770c625d..75a4edd447c70 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/latency.ts @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; import { IKibanaSearchRequest } from '../../../../../src/plugins/data/common'; import type { LatencyCorrelationsParams } from '../../../../plugins/apm/common/search_strategies/latency_correlations/types'; -import type { SearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types'; +import type { RawSearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types'; import { APM_SEARCH_STRATEGIES } from '../../../../plugins/apm/common/search_strategies/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -22,16 +22,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('legacySupertestAsApmReadUser'); const getRequestBody = () => { - const request: IKibanaSearchRequest = { - params: { - environment: 'ENVIRONMENT_ALL', - start: '2020', - end: '2021', - kuery: '', - percentileThreshold: 95, - analyzeCorrelations: true, - }, - }; + const request: IKibanaSearchRequest = + { + params: { + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + kuery: '', + percentileThreshold: 95, + analyzeCorrelations: true, + }, + }; return { batch: [ diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 09f4e2596ea46..f68a49658f2ee 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -175,6 +175,10 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./transactions/error_rate')); }); + describe('transactions/latency_overall_distribution', function () { + loadTestFile(require.resolve('./transactions/latency_overall_distribution')); + }); + describe('transactions/latency', function () { loadTestFile(require.resolve('./transactions/latency')); }); diff --git a/x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.ts b/x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.ts new file mode 100644 index 0000000000000..c915ac8911e37 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.ts @@ -0,0 +1,65 @@ +/* + * 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'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + + const endpoint = 'GET /internal/apm/latency/overall_distribution'; + + // This matches the parameters used for the other tab's search strategy approach in `../correlations/*`. + const getOptions = () => ({ + params: { + query: { + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + kuery: '', + percentileThreshold: '95', + }, + }, + }); + + registry.when( + 'latency overall distribution without data', + { config: 'trial', archives: [] }, + () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.be(200); + expect(response.body?.percentileThresholdValue).to.be(undefined); + expect(response.body?.overallHistogram?.length).to.be(undefined); + }); + } + ); + + registry.when( + 'latency overall distribution with data and default args', + // This uses the same archive used for the other tab's search strategy approach in `../correlations/*`. + { config: 'trial', archives: ['8.0.0'] }, + () => { + it('returns percentileThresholdValue and overall histogram', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.eql(200); + // This matches the values returned for the other tab's search strategy approach in `../correlations/*`. + expect(response.body?.percentileThresholdValue).to.be(1309695.875); + expect(response.body?.overallHistogram?.length).to.be(101); + }); + } + ); +}