diff --git a/public/components/trace_analytics/components/common/__tests__/helper_functions.test.tsx b/public/components/trace_analytics/components/common/__tests__/helper_functions.test.tsx index 16e6d7177f..217c3b8f47 100644 --- a/public/components/trace_analytics/components/common/__tests__/helper_functions.test.tsx +++ b/public/components/trace_analytics/components/common/__tests__/helper_functions.test.tsx @@ -13,6 +13,7 @@ import { TEST_SERVICE_MAP_GRAPH, } from '../../../../../../test/constants'; import { + appendModeToTraceViewUri, calculateTicks, filtersToDsl, fixedIntervalToMilli, @@ -22,6 +23,7 @@ import { getServiceMapGraph, getServiceMapScaleColor, getServiceMapTargetResources, + getTimestampPrecision, milliToNanoSec, minFixedInterval, MissingConfigurationMessage, @@ -195,4 +197,56 @@ describe('Helper functions', () => { expect(result).toEqual([]); }); }); + + describe('getTimestampPrecision', () => { + it('returns "millis" for 13-digit timestamps', () => { + expect(getTimestampPrecision(1703599200000)).toEqual('millis'); + }); + + it('returns "micros" for 16-digit timestamps', () => { + expect(getTimestampPrecision(1703599200000000)).toEqual('micros'); + }); + + it('returns "nanos" for 19-digit timestamps', () => { + expect(getTimestampPrecision(1703599200000000000)).toEqual('nanos'); + }); + + it('returns "millis" for invalid or missing timestamps', () => { + expect(getTimestampPrecision((undefined as unknown) as number)).toEqual('millis'); + expect(getTimestampPrecision(123)).toEqual('millis'); + }); + }); + + describe('appendModeToTraceViewUri', () => { + const mockGetTraceViewUri = (traceId: string) => `#/traces/${traceId}`; + + it('appends mode to the URI when mode is provided', () => { + const result = appendModeToTraceViewUri('123', mockGetTraceViewUri, 'data_prepper'); + expect(result).toEqual('#/traces/123?mode=data_prepper'); + }); + + it('appends mode correctly for hash router URIs with existing query params', () => { + const result = appendModeToTraceViewUri('123', (id) => `#/traces/${id}?foo=bar`, 'jaeger'); + expect(result).toEqual('#/traces/123?foo=bar&mode=jaeger'); + }); + + it('does not append mode if not provided', () => { + const result = appendModeToTraceViewUri('123', mockGetTraceViewUri, null); + expect(result).toEqual('#/traces/123'); + }); + + it('handles URIs without a hash router', () => { + const result = appendModeToTraceViewUri( + '123', + (id) => `/traces/${id}`, + 'custom_data_prepper' + ); + expect(result).toEqual('/traces/123?mode=custom_data_prepper'); + }); + + it('handles URIs without a hash router and existing query params', () => { + const result = appendModeToTraceViewUri('123', (id) => `/traces/${id}?foo=bar`, 'jaeger'); + expect(result).toEqual('/traces/123?foo=bar&mode=jaeger'); + }); + }); }); diff --git a/public/components/trace_analytics/components/common/helper_functions.tsx b/public/components/trace_analytics/components/common/helper_functions.tsx index ec7ad6e17b..74a3676653 100644 --- a/public/components/trace_analytics/components/common/helper_functions.tsx +++ b/public/components/trace_analytics/components/common/helper_functions.tsx @@ -119,6 +119,41 @@ export function nanoToMilliSec(nano: number) { return nano / 1000000; } +export const getTimestampPrecision = (timestamp: number): 'millis' | 'micros' | 'nanos' => { + if (!timestamp) return 'millis'; + + const digitCount = timestamp.toString().length; + + // For Unix timestamps: + // 13 digits = milliseconds (e.g., 1703599200000) + // 16 digits = microseconds (e.g., 1703599200000000) + // 19 digits = nanoseconds (e.g., 1703599200000000000) + switch (digitCount) { + case 13: + return 'millis'; + case 16: + return 'micros'; + case 19: + return 'nanos'; + default: + return 'millis'; + } +}; + +export const appendModeToTraceViewUri = ( + traceId: string, + getTraceViewUri?: (traceId: string) => string, + traceMode?: string | null +): string => { + const baseUri = getTraceViewUri ? getTraceViewUri(traceId) : ''; + const separator = baseUri.includes('?') ? '&' : '?'; + + if (traceMode) { + return `${baseUri}${separator}mode=${encodeURIComponent(traceMode)}`; + } + return baseUri; +}; + export function microToMilliSec(micro: number) { if (typeof micro !== 'number') return 0; return micro / 1000; diff --git a/public/components/trace_analytics/components/common/plots/service_map.tsx b/public/components/trace_analytics/components/common/plots/service_map.tsx index ae53fb5f85..f75f1e8403 100644 --- a/public/components/trace_analytics/components/common/plots/service_map.tsx +++ b/public/components/trace_analytics/components/common/plots/service_map.tsx @@ -123,17 +123,6 @@ export function ServiceMap({ ); }, [filters, focusedService]); - const onChangeSelectable = (value: React.SetStateAction>>) => { - // if the change is changing for the first time then callback servicemap with metrics - if (selectableValue.length === 0 && value.length !== 0) { - if (includeMetricsCallback) { - includeMetricsCallback(); - } - } - setIdSelected(value); - setSelectableValue(value); - }; - const metricOptions: Array> = [ { value: 'latency', @@ -149,6 +138,19 @@ export function ServiceMap({ }, ]; + // For the traces custom page + useEffect(() => { + if (!selectableValue || selectableValue.length === 0) { + // Set to the first option ("latency") and trigger the onChange function + const defaultValue = metricOptions[0].value; + setSelectableValue(defaultValue); // Update the state + setIdSelected(defaultValue); // Propagate the default to parent + if (includeMetricsCallback) { + includeMetricsCallback(); + } + } + }, []); + const removeFilter = (field: string, value: string) => { if (!setFilters) return; const updatedFilters = filters.filter( @@ -504,7 +506,10 @@ export function ServiceMap({ compressed options={metricOptions} valueOfSelected={selectableValue} - onChange={(value) => onChangeSelectable(value)} + onChange={(value) => { + setSelectableValue(value); + setIdSelected(value); + }} /> )} diff --git a/public/components/trace_analytics/components/dashboard/mode_picker.tsx b/public/components/trace_analytics/components/dashboard/mode_picker.tsx index de316f76d7..762d030dc1 100644 --- a/public/components/trace_analytics/components/dashboard/mode_picker.tsx +++ b/public/components/trace_analytics/components/dashboard/mode_picker.tsx @@ -62,6 +62,28 @@ export function DataSourcePicker(props: { ); }; + const updateUrlWithMode = (key: TraceAnalyticsMode) => { + const currentUrl = window.location.href.split('#')[0]; + const hash = window.location.hash; + + if (hash) { + const [hashPath, hashQueryString] = hash.substring(1).split('?'); + const queryParams = new URLSearchParams(hashQueryString || ''); + queryParams.set('mode', key); + + const newHash = `${hashPath}?${queryParams.toString()}`; + const newUrl = `${currentUrl}#${newHash}`; + window.history.replaceState(null, '', newUrl); + } else { + // Non-hash-based URL + const queryParams = new URLSearchParams(window.location.search); + queryParams.set('mode', key); + + const newUrl = `${currentUrl}?${queryParams.toString()}`; + window.history.replaceState(null, '', newUrl); + } + }; + return ( <> @@ -3930,6 +3931,7 @@ exports[`Traces component renders jaeger traces page 1`] = ` jaegerIndicesExist={true} loading={true} mode="jaeger" + page="traces" refresh={[Function]} > @@ -6217,6 +6219,7 @@ exports[`Traces component renders traces page 1`] = ` items={Array []} loading={true} mode="data_prepper" + page="traces" refresh={[Function]} > diff --git a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces_table.test.tsx.snap b/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces_table.test.tsx.snap index eb4bdd4127..5d76bf5912 100644 --- a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces_table.test.tsx.snap +++ b/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces_table.test.tsx.snap @@ -1121,12 +1121,13 @@ exports[`Traces table component renders jaeger traces table 1`] = ` > - + @@ -2686,12 +2687,13 @@ exports[`Traces table component renders traces table 1`] = ` > - + diff --git a/public/components/trace_analytics/components/traces/__tests__/traces_table.test.tsx b/public/components/trace_analytics/components/traces/__tests__/traces_table.test.tsx index 33f01f1ea6..3ec9870599 100644 --- a/public/components/trace_analytics/components/traces/__tests__/traces_table.test.tsx +++ b/public/components/trace_analytics/components/traces/__tests__/traces_table.test.tsx @@ -13,8 +13,7 @@ describe('Traces table component', () => { it('renders empty traces table message', () => { const refresh = jest.fn(); - const getTraceViewUri = (item: any) => - location.assign(`#/trace_analytics/traces/${encodeURIComponent(item)}`); + const getTraceViewUri = (item: any) => `#/trace_analytics/traces/${encodeURIComponent(item)}`; const noIndicesTable = mount( { actions: '#', }, ]; - const getTraceViewUri = (item: any) => - location.assign(`#/trace_analytics/traces/${encodeURIComponent(item)}`); + const getTraceViewUri = (item: any) => `#/trace_analytics/traces/${encodeURIComponent(item)}`; const refresh = jest.fn(); const wrapper = mount( { actions: '#', }, ]; - const getTraceViewUri = (item: any) => - location.assign(`#/trace_analytics/traces/${encodeURIComponent(item)}`); + const getTraceViewUri = (item: any) => `#/trace_analytics/traces/${encodeURIComponent(item)}`; const refresh = jest.fn(); const wrapper = mount( void; filteredService: string; setFilteredService: React.Dispatch>; + filters: FilterType[]; + setFilters: (filters: FilterType[]) => void; } export const ServicesList = ({ @@ -28,11 +30,31 @@ export const ServicesList = ({ addFilter, filteredService, setFilteredService, + filters = [], + setFilters, }: ServicesListProps) => { - const [options, setOptions] = useState>([]); + const [options, setOptions] = useState>([]); + + const removeFilter = (field: string, value: string) => { + if (!setFilters) return; + const updatedFilters = filters.filter( + (filter) => !(filter.field === field && filter.value === value) + ); + setFilters(updatedFilters); + }; const nameColumnAction = (serviceName: string) => { if (!addFilter) return; + + // Check if the service is already selected + if (filteredService === serviceName) { + // Remove the filter if the service is deselected + removeFilter('serviceName', filteredService); + setFilteredService(''); // Reset the selected service + return; + } + + // Add the filter if a new service is selected addFilter({ field: 'serviceName', operator: 'is', @@ -58,14 +80,14 @@ export const ServicesList = ({ ); useEffect(() => { + // Update selectable options based on the current filtered service setOptions( - Object.keys(serviceMap).map((key) => { - return filteredService === key - ? { label: key, checked: 'on', bordered: false } - : { label: key, bordered: false }; - }) + Object.keys(serviceMap).map((key) => ({ + label: key, + checked: filteredService === key ? 'on' : undefined, + })) ); - }, [serviceMap]); + }, [serviceMap, filteredService]); return ( @@ -74,14 +96,26 @@ export const ServicesList = ({
{ - setOptions(newOptions); - nameColumnAction(newOptions.filter((option) => option.checked === 'on')[0].label); + const selectedOption = newOptions.find((option) => option.checked === 'on'); + + // Handle deselection + if (!selectedOption) { + nameColumnAction(filteredService); + setOptions(newOptions); + return; + } + + // Handle selection + if (selectedOption) { + nameColumnAction(selectedOption.label); + setOptions(newOptions); + } }} singleSelection={true} > diff --git a/public/components/trace_analytics/components/traces/trace_table_helpers.tsx b/public/components/trace_analytics/components/traces/trace_table_helpers.tsx index 2ff58d50d5..6d506714ed 100644 --- a/public/components/trace_analytics/components/traces/trace_table_helpers.tsx +++ b/public/components/trace_analytics/components/traces/trace_table_helpers.tsx @@ -18,7 +18,7 @@ import moment from 'moment'; import React from 'react'; import { TRACE_ANALYTICS_DATE_FORMAT } from '../../../../../common/constants/trace_analytics'; import { TraceAnalyticsMode, TraceQueryMode } from '../../../../../common/types/trace_analytics'; -import { nanoToMilliSec } from '../common/helper_functions'; +import { appendModeToTraceViewUri, nanoToMilliSec } from '../common/helper_functions'; export const fetchDynamicColumns = (columnItems: string[]) => { return columnItems @@ -73,28 +73,36 @@ export const getTableColumns = ( '-' ); - const renderTraceLinkField = (item: string) => ( - - - openTraceFlyout(item) })} - > - - {item} - - - - - - {(copy) => ( - - )} - - - - ); + const renderTraceLinkField = (item: string) => { + // Extract the current mode from the URL or session storage + const currentUrl = window.location.href; + const traceMode = + new URLSearchParams(currentUrl.split('?')[1]).get('mode') || + sessionStorage.getItem('TraceAnalyticsMode'); + + return ( + + + openTraceFlyout(item) })} + > + + {item} + + + + + + {(copy) => ( + + )} + + + + ); + }; const renderErrorsField = (item: number) => item == null ? ( diff --git a/public/components/trace_analytics/components/traces/traces_content.tsx b/public/components/trace_analytics/components/traces/traces_content.tsx index b809372fc7..4469c20563 100644 --- a/public/components/trace_analytics/components/traces/traces_content.tsx +++ b/public/components/trace_analytics/components/traces/traces_content.tsx @@ -290,6 +290,7 @@ export function TracesContent(props: TracesProps) { openTraceFlyout={openTraceFlyout} jaegerIndicesExist={jaegerIndicesExist} dataPrepperIndicesExist={dataPrepperIndicesExist} + page={page} /> )} @@ -301,6 +302,8 @@ export function TracesContent(props: TracesProps) { void; jaegerIndicesExist: boolean; dataPrepperIndicesExist: boolean; + page?: 'traces' | 'app'; } export function TracesTable(props: TracesTableProps) { @@ -54,6 +56,12 @@ export function TracesTable(props: TracesTableProps) { }; const columns = useMemo(() => { + // Extract the current mode from the URL or session storage + const currentUrl = window.location.href; + const traceMode = + new URLSearchParams(currentUrl.split('?')[1]).get('mode') || + sessionStorage.getItem('TraceAnalyticsMode'); + if (mode === 'data_prepper' || mode === 'custom_data_prepper') { return [ { @@ -67,7 +75,9 @@ export function TracesTable(props: TracesTableProps) { openTraceFlyout(item) })} > @@ -158,7 +168,7 @@ export function TracesTable(props: TracesTableProps) { openTraceFlyout(item) })} > diff --git a/public/components/trace_analytics/home.tsx b/public/components/trace_analytics/home.tsx index ccdb45c391..3d3e7ed393 100644 --- a/public/components/trace_analytics/home.tsx +++ b/public/components/trace_analytics/home.tsx @@ -87,7 +87,7 @@ export const Home = (props: HomeProps) => { const [jaegerIndicesExist, setJaegerIndicesExist] = useState(false); const [attributesFilterFields, setAttributesFilterFields] = useState([]); const [mode, setMode] = useState( - (sessionStorage.getItem('TraceAnalyticsMode') as TraceAnalyticsMode) || 'jaeger' + (sessionStorage.getItem('TraceAnalyticsMode') as TraceAnalyticsMode) || 'data_prepper' ); const storedFilters = sessionStorage.getItem('TraceAnalyticsFilters'); const [query, setQuery] = useState(sessionStorage.getItem('TraceAnalyticsQuery') || ''); @@ -210,6 +210,10 @@ export const Home = (props: HomeProps) => { ]); }; + const isValidTraceAnalyticsMode = (urlMode: string | null): urlMode is TraceAnalyticsMode => { + return ['jaeger', 'data_prepper', 'custom_data_prepper'].includes(urlMode || ''); + }; + useEffect(() => { handleDataPrepperIndicesExistRequest( props.http, @@ -406,6 +410,8 @@ export const Home = (props: HomeProps) => { render={(_routerProps) => { const queryParams = new URLSearchParams(window.location.href.split('?')[1]); const traceId = queryParams.get('traceId'); + const traceModeFromURL = queryParams.get('mode'); + const traceMode = isValidTraceAnalyticsMode(traceModeFromURL) ? traceModeFromURL : mode; const SideBarComponent = !isNavGroupEnabled ? TraceSideBar : React.Fragment; if (!traceId) { @@ -421,6 +427,7 @@ export const Home = (props: HomeProps) => { tracesTableMode={tracesTableMode} setTracesTableMode={setTracesTableMode} {...commonProps} + mode={((traceMode as unknown) as TraceAnalyticsMode) || mode} /> ); @@ -431,7 +438,7 @@ export const Home = (props: HomeProps) => { chrome={props.chrome} http={props.http} traceId={decodeURIComponent(traceId)} - mode={mode} + mode={((traceMode as unknown) as TraceAnalyticsMode) || mode} dataSourceMDSId={dataSourceMDSId} dataSourceManagement={props.dataSourceManagement} setActionMenu={props.setActionMenu} @@ -449,6 +456,10 @@ export const Home = (props: HomeProps) => { render={(_routerProps) => { const queryParams = new URLSearchParams(window.location.href.split('?')[1]); const serviceId = queryParams.get('serviceId'); + const serviceModeFromURL = queryParams.get('mode'); + const serviceMode = isValidTraceAnalyticsMode(serviceModeFromURL) + ? serviceModeFromURL + : mode; const SideBarComponent = !isNavGroupEnabled ? TraceSideBar : React.Fragment; if (!serviceId) { @@ -462,6 +473,7 @@ export const Home = (props: HomeProps) => { toasts={toasts} dataSourceMDSId={dataSourceMDSId} {...commonProps} + mode={((serviceMode as unknown) as TraceAnalyticsMode) || mode} /> ); @@ -470,6 +482,7 @@ export const Home = (props: HomeProps) => { { for (const addedFilter of filters) { if ( diff --git a/public/components/trace_analytics/requests/services_request_handler.ts b/public/components/trace_analytics/requests/services_request_handler.ts index 8ba0375c3a..0995b7e0b4 100644 --- a/public/components/trace_analytics/requests/services_request_handler.ts +++ b/public/components/trace_analytics/requests/services_request_handler.ts @@ -22,6 +22,7 @@ import { getServiceTrendsQuery, } from './queries/services_queries'; import { handleDslRequest } from './request_handler'; +import { coreRefs } from '../../../../public/framework/core_refs'; export const handleServicesRequest = async ( http: HttpSetup, @@ -71,7 +72,13 @@ export const handleServicesRequest = async ( .then((newItems) => { setItems(newItems); }) - .catch((error) => console.error(error)); + .catch((error) => { + console.error('Error in handleServicesRequest:', error); + coreRefs.core?.notifications.toasts.addError(error, { + title: 'Failed to retrieve services', + toastLifeTimeMs: 10000, + }); + }); }; export const handleServiceMapRequest = async ( @@ -92,8 +99,14 @@ export const handleServiceMapRequest = async ( } const map: ServiceObject = {}; let id = 1; - await handleDslRequest(http, null, getServiceNodesQuery(mode), mode, dataSourceMDSId) - .then((response) => + const serviceNodesResponse = await handleDslRequest( + http, + null, + getServiceNodesQuery(mode), + mode, + dataSourceMDSId + ) + .then((response) => { response.aggregations.service_name.buckets.map( (bucket: any) => (map[bucket.key] = { @@ -106,9 +119,22 @@ export const handleServiceMapRequest = async ( targetServices: [], destServices: [], }) - ) - ) - .catch((error) => console.error(error)); + ); + return true; + }) + .catch((error) => { + console.error('Error retrieving service nodes:', error); + coreRefs.core?.notifications.toasts.addError(error, { + title: 'Failed to retrieve service nodes', + toastLifeTimeMs: 10000, + }); + return false; + }); + + // Early return if service node not found + if (!serviceNodesResponse) { + return map; + } const targets = {}; await handleDslRequest(http, null, getServiceEdgesQuery('target', mode), mode, dataSourceMDSId) @@ -121,7 +147,14 @@ export const handleServiceMapRequest = async ( }); }) ) - .catch((error) => console.error(error)); + .catch((error) => { + console.error('Error retrieving target edges:', error); + coreRefs.core?.notifications.toasts.addError(error, { + title: 'Failed to retrieve target edges', + toastLifeTimeMs: 10000, + }); + }); + await handleDslRequest( http, null, @@ -146,24 +179,37 @@ export const handleServiceMapRequest = async ( }) ) ) - .catch((error) => console.error(error)); + .catch((error) => { + console.error('Error retrieving destination edges:', error); + coreRefs.core?.notifications.toasts.addError(error, { + title: 'Failed to retrieve destination edges', + toastLifeTimeMs: 10000, + }); + }); if (includeMetrics) { - // service map handles DSL differently - const latencies = await handleDslRequest( - http, - DSL, - getServiceMetricsQuery(DSL, Object.keys(map), map, mode), - mode, - dataSourceMDSId - ); - latencies.aggregations.service_name.buckets.map((bucket: any) => { - map[bucket.key].latency = bucket.average_latency.value; - map[bucket.key].error_rate = round(bucket.error_rate.value, 2) || 0; - map[bucket.key].throughput = bucket.doc_count; - if (minutesInDateRange != null) - map[bucket.key].throughputPerMinute = round(bucket.doc_count / minutesInDateRange, 2); - }); + try { + const latencies = await handleDslRequest( + http, + DSL, + getServiceMetricsQuery(DSL, Object.keys(map), map, mode), + mode, + dataSourceMDSId + ); + latencies.aggregations.service_name.buckets.map((bucket: any) => { + map[bucket.key].latency = bucket.average_latency.value; + map[bucket.key].error_rate = round(bucket.error_rate.value, 2) || 0; + map[bucket.key].throughput = bucket.doc_count; + if (minutesInDateRange != null) + map[bucket.key].throughputPerMinute = round(bucket.doc_count / minutesInDateRange, 2); + }); + } catch (error) { + console.error('Error retrieving service metrics:', error); + coreRefs.core?.notifications.toasts.addError(error, { + title: 'Failed to retrieve service metrics', + toastLifeTimeMs: 10000, + }); + } } if (currService) { @@ -180,7 +226,13 @@ export const handleServiceMapRequest = async ( } map[currService].relatedServices = [...relatedServices]; }) - .catch((error) => console.error(error)); + .catch((error) => { + console.error('Error retrieving related services:', error); + coreRefs.core?.notifications.toasts.addError(error, { + title: 'Failed to retrieve related services', + toastLifeTimeMs: 10000, + }); + }); } if (setItems) setItems(map); @@ -222,7 +274,13 @@ export const handleServiceViewRequest = ( .then((newFields) => { setFields(newFields); }) - .catch((error) => console.error(error)); + .catch((error) => { + console.error('Error in handleServiceViewRequest:', error); + coreRefs.core?.notifications.toasts.addError(error, { + title: 'Failed to retrieve service view data', + toastLifeTimeMs: 10000, + }); + }); }; export const handleServiceTrendsRequest = ( @@ -324,5 +382,11 @@ export const handleServiceTrendsRequest = ( }); setItems(parsedResult); }) - .catch((error) => console.error(error)); + .catch((error) => { + console.error('Error in handleServiceTrendsRequest:', error); + coreRefs.core?.notifications.toasts.addError(error, { + title: 'Failed to retrieve service trends', + toastLifeTimeMs: 10000, + }); + }); }; diff --git a/public/components/trace_analytics/requests/traces_request_handler.ts b/public/components/trace_analytics/requests/traces_request_handler.ts index 883014afbc..6706b4fa8f 100644 --- a/public/components/trace_analytics/requests/traces_request_handler.ts +++ b/public/components/trace_analytics/requests/traces_request_handler.ts @@ -14,7 +14,11 @@ import { HttpSetup } from '../../../../../../src/core/public'; import { BarOrientation } from '../../../../common/constants/shared'; import { TRACE_ANALYTICS_DATE_FORMAT } from '../../../../common/constants/trace_analytics'; import { TraceAnalyticsMode, TraceQueryMode } from '../../../../common/types/trace_analytics'; -import { microToMilliSec, nanoToMilliSec } from '../components/common/helper_functions'; +import { + getTimestampPrecision, + microToMilliSec, + nanoToMilliSec, +} from '../components/common/helper_functions'; import { SpanSearchParams } from '../components/traces/span_detail_table'; import { getCustomIndicesTracesQuery, @@ -27,6 +31,7 @@ import { getTracesQuery, } from './queries/traces_queries'; import { handleDslRequest } from './request_handler'; +import { coreRefs } from '../../../../public/framework/core_refs'; export const handleCustomIndicesTracesRequest = async ( http: HttpSetup, @@ -80,7 +85,13 @@ export const handleCustomIndicesTracesRequest = async ( setColumns([...newItems[0]]); setItems(newItems[1]); }) - .catch((error) => console.error(error)); + .catch((error) => { + console.error('Error in handleCustomIndicesTracesRequest:', error); + coreRefs.core?.notifications.toasts.addError(error, { + title: 'Failed to retrieve custom indices traces', + toastLifeTimeMs: 10000, + }); + }); }; export const handleTracesRequest = async ( @@ -141,6 +152,7 @@ export const handleTracesRequest = async ( const percentileRanges = percentileRangesResult.status === 'fulfilled' ? percentileRangesResult.value : {}; const response = responseResult.value; + return response.aggregations.traces.buckets.map((bucket: any) => { if (mode === 'data_prepper' || mode === 'custom_data_prepper') { return { @@ -168,7 +180,13 @@ export const handleTracesRequest = async ( .then((newItems) => { setItems(newItems); }) - .catch((error) => console.error(error)); + .catch((error) => { + console.error('Error in handleTracesRequest:', error); + coreRefs.core?.notifications.toasts.addError(error, { + title: 'Failed to retrieve traces', + toastLifeTimeMs: 10000, + }); + }); }; export const handleTraceViewRequest = ( @@ -181,6 +199,11 @@ export const handleTraceViewRequest = ( ) => { handleDslRequest(http, null, getTracesQuery(mode, traceId), mode, dataSourceMDSId) .then(async (response) => { + // Check if the mode hasn't been set first + if (mode === 'jaeger' && !response?.aggregations?.service_type?.buckets) { + console.warn('No traces or aggregations found.'); + return []; + } const bucket = response.aggregations.traces.buckets[0]; return { trace_id: bucket.key, @@ -197,7 +220,13 @@ export const handleTraceViewRequest = ( .then((newFields) => { setFields(newFields); }) - .catch((error) => console.error(error)); + .catch((error) => { + console.error('Error in handleTraceViewRequest:', error); + coreRefs.core?.notifications.toasts.addError(error, { + title: `Failed to retrieve trace view for trace ID: ${traceId}`, + toastLifeTimeMs: 10000, + }); + }); }; // setColorMap sets serviceName to color mappings @@ -226,8 +255,14 @@ export const handleServicesPieChartRequest = async ( const colorMap: any = {}; let index = 0; await handleDslRequest(http, null, getServiceBreakdownQuery(traceId, mode), mode, dataSourceMDSId) - .then((response) => - Promise.all( + .then((response) => { + // Check if the mode hasn't been set first + if (mode === 'jaeger' && !response?.aggregations?.service_type?.buckets) { + console.warn(`No service breakdown found for trace ID: ${traceId}`); + return []; + } + + return Promise.all( response.aggregations.service_type.buckets.map((bucket: any) => { colorMap[bucket.key] = colors[index++ % colors.length]; return { @@ -237,9 +272,10 @@ export const handleServicesPieChartRequest = async ( benchmark: 0, }; }) - ) - ) + ); + }) .then((newItems) => { + if (!newItems.length) return; // No data to process const latencySum = newItems.map((item) => item.value).reduce((a, b) => a + b, 0); return [ { @@ -258,10 +294,18 @@ export const handleServicesPieChartRequest = async ( ]; }) .then((newItems) => { - setServiceBreakdownData(newItems); - setColorMap(colorMap); + if (newItems) { + setServiceBreakdownData(newItems); + setColorMap(colorMap); + } }) - .catch((error) => console.error(error)); + .catch((error) => { + console.error('Error in handleServicesPieChartRequest:', error); + coreRefs.core?.notifications.toasts.addError(error, { + title: `Failed to retrieve service breakdown for trace ID: ${traceId}`, + toastLifeTimeMs: 10000, + }); + }); }; export const handleSpansGanttRequest = ( @@ -276,7 +320,13 @@ export const handleSpansGanttRequest = ( handleDslRequest(http, spanFiltersDSL, getSpanDetailQuery(mode, traceId), mode, dataSourceMDSId) .then((response) => hitsToSpanDetailData(response.hits.hits, colorMap, mode)) .then((newItems) => setSpanDetailData(newItems)) - .catch((error) => console.error(error)); + .catch((error) => { + console.error('Error in handleSpansGanttRequest:', error); + coreRefs.core?.notifications.toasts.addError(error, { + title: `Failed to retrieve spans Gantt chart for trace ID: ${traceId}`, + toastLifeTimeMs: 10000, + }); + }); }; export const handleSpansFlyoutRequest = ( @@ -290,7 +340,13 @@ export const handleSpansFlyoutRequest = ( .then((response) => { setItems(response?.hits.hits?.[0]?._source); }) - .catch((error) => console.error(error)); + .catch((error) => { + console.error('Error in handleSpansFlyoutRequest:', error); + coreRefs.core?.notifications.toasts.addError(error, { + title: `Failed to retrieve span details for span ID: ${spanId}`, + toastLifeTimeMs: 10000, + }); + }); }; const hitsToSpanDetailData = async (hits: any, colorMap: any, mode: TraceAnalyticsMode) => { @@ -301,17 +357,35 @@ const hitsToSpanDetailData = async (hits: any, colorMap: any, mode: TraceAnalyti }; if (hits.length === 0) return data; - const minStartTime = - mode === 'jaeger' - ? microToMilliSec(hits[hits.length - 1].sort[0]) - : nanoToMilliSec(hits[hits.length - 1].sort[0]); + const timestampPrecision = getTimestampPrecision(hits[hits.length - 1].sort[0]); + + const minStartTime = (() => { + switch (timestampPrecision) { + case 'micros': + return microToMilliSec(hits[hits.length - 1].sort[0]); + case 'nanos': + return nanoToMilliSec(hits[hits.length - 1].sort[0]); + default: + // 'millis' + return hits[hits.length - 1].sort[0]; + } + })(); + let maxEndTime = 0; hits.forEach((hit: any) => { - const startTime = - mode === 'jaeger' - ? microToMilliSec(hit.sort[0]) - minStartTime - : nanoToMilliSec(hit.sort[0]) - minStartTime; + const startTime = (() => { + switch (timestampPrecision) { + case 'micros': + return microToMilliSec(hit.sort[0]) - minStartTime; + case 'nanos': + return nanoToMilliSec(hit.sort[0]) - minStartTime; + default: + // 'millis' + return hit.sort[0] - minStartTime; + } + })(); + const duration = mode === 'jaeger' ? round(microToMilliSec(hit._source.duration), 2) @@ -387,7 +461,13 @@ export const handlePayloadRequest = ( ) => { handleDslRequest(http, null, getPayloadQuery(mode, traceId), mode, dataSourceMDSId) .then((response) => setPayloadData(JSON.stringify(response.hits.hits, null, 2))) - .catch((error) => console.error(error)); + .catch((error) => { + console.error('Error in handlePayloadRequest:', error); + coreRefs.core?.notifications.toasts.addError(error, { + title: `Failed to retrieve payload for trace ID: ${traceId}`, + toastLifeTimeMs: 10000, + }); + }); }; export const handleSpansRequest = ( @@ -404,5 +484,11 @@ export const handleSpansRequest = ( setItems(response.hits.hits.map((hit: any) => hit._source)); setTotal(response.hits.total?.value || 0); }) - .catch((error) => console.error(error)); + .catch((error) => { + console.error('Error in handleSpansRequest:', error); + coreRefs.core?.notifications.toasts.addError(error, { + title: 'Failed to retrieve spans', + toastLifeTimeMs: 10000, + }); + }); };