feat: added 10 minute time range for traces co-relation in api monitoring (#7815)

* feat: added 10 minute time range for traces co-relation in api monitoring

* feat: url sharing for domain details drawer w/o filters for endpoint stats

* feat: added endpoint details persistence

* feat: external services to api monitoring co relation - 0

* feat: added api co relations to other panels on external services

* fix: cosmetic fix

* feat: addded tests for url sharing utils

* feat: minor cosmetic changes

* fix: changed traces co relation window from 10 minutes to 5 minutes

* fix: minor copy changes

* fix: pr comments

* fix: minor bug fix

* fix: pr comments improvements
This commit is contained in:
Sahil Khan 2025-05-14 17:12:45 +05:30 committed by GitHub
parent 81b8f93177
commit 16938c6cc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 886 additions and 157 deletions

View File

@ -69,5 +69,5 @@
"METRICS_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
"API_MONITORING": "SigNoz | API Monitoring"
"API_MONITORING": "SigNoz | Third Party API"
}

View File

@ -7,10 +7,12 @@ import {
import GridCard from 'container/GridCardLayout/GridCard';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { isEqual } from 'lodash-es';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { useApiMonitoringParams } from '../../../queryParams';
import { SPAN_ATTRIBUTES, VIEWS } from './constants';
function AllEndPoints({
@ -35,6 +37,7 @@ function AllEndPoints({
initialFilters: IBuilderQuery['filters'];
setInitialFiltersEndPointStats: (filters: IBuilderQuery['filters']) => void;
}): JSX.Element {
const [params, setParams] = useApiMonitoringParams();
const [groupBySearchValue, setGroupBySearchValue] = useState<string>('');
const [allAvailableGroupByOptions, setAllAvailableGroupByOptions] = useState<{
[key: string]: any;
@ -55,87 +58,38 @@ function AllEndPoints({
{ value: string; label: string }[]
>([]);
const handleGroupByChange = useCallback(
(value: IBuilderQuery['groupBy']) => {
const newGroupBy = [];
for (let index = 0; index < value.length; index++) {
const element = (value[index] as unknown) as string;
// Check if the key exists in our cached options first
if (allAvailableGroupByOptions[element]) {
newGroupBy.push(allAvailableGroupByOptions[element]);
} else {
// If not found in cache, check the current filtered results
const key = groupByFiltersData?.payload?.attributeKeys?.find(
(key) => key.key === element,
);
if (key) {
newGroupBy.push(key);
}
}
}
setGroupBy(newGroupBy);
setGroupBySearchValue('');
},
[groupByFiltersData, setGroupBy, allAvailableGroupByOptions],
);
useEffect(() => {
if (groupByFiltersData?.payload) {
// Update dropdown options
setGroupByOptions(
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
value: filter.key,
label: filter.key,
})) || [],
);
// Cache all available options to preserve selected values using functional update
// to avoid dependency on allAvailableGroupByOptions
setAllAvailableGroupByOptions((prevOptions) => {
const newOptions = { ...prevOptions };
groupByFiltersData?.payload?.attributeKeys?.forEach((filter) => {
newOptions[filter.key] = filter;
});
return newOptions;
});
}
}, [groupByFiltersData]); // Only depends on groupByFiltersData now
// Cache existing selected options on component mount
useEffect(() => {
if (groupBy && groupBy.length > 0) {
setAllAvailableGroupByOptions((prevOptions) => {
const newOptions = { ...prevOptions };
groupBy.forEach((option) => {
newOptions[option.key] = option;
});
return newOptions;
});
}
}, [groupBy]); // Removed allAvailableGroupByOptions from dependencies
const currentQuery = initialQueriesMap[DataSource.TRACES];
// Local state for filters, combining endpoint filter and search filters
// --- FILTERS STATE SYNC ---
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
if (params.allEndpointsLocalFilters) {
return params.allEndpointsLocalFilters;
}
// Initialize filters based on the initial endPointName prop
const initialItems = [...initialFilters.items];
return { op: 'AND', items: initialItems };
});
// Sync params to local filters state on param change
useEffect(() => {
if (
params.allEndpointsLocalFilters &&
!isEqual(params.allEndpointsLocalFilters, filters)
) {
setFilters(params.allEndpointsLocalFilters);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params.allEndpointsLocalFilters]);
// Handler for changes from the QueryBuilderSearchV2 component
const handleFilterChange = useCallback(
(newFilters: IBuilderQuery['filters']): void => {
// 1. Update local filters state immediately
setFilters(newFilters);
setParams({ allEndpointsLocalFilters: newFilters });
},
[], // Dependencies for the callback
[setParams],
);
const currentQuery = initialQueriesMap[DataSource.TRACES];
const updatedCurrentQuery = useMemo(
() => ({
...currentQuery,
@ -160,6 +114,91 @@ function AllEndPoints({
[groupBy, domainName, filters],
);
// --- GROUP BY STATE SYNC (existing) ---
const handleGroupByChange = useCallback(
(value: IBuilderQuery['groupBy']) => {
const newGroupBy = [];
for (let index = 0; index < value.length; index++) {
const element = (value[index] as unknown) as string;
// Check if the key exists in our cached options first
if (allAvailableGroupByOptions[element]) {
newGroupBy.push(allAvailableGroupByOptions[element]);
} else {
// If not found in cache, check the current filtered results
const key = groupByFiltersData?.payload?.attributeKeys?.find(
(key) => key.key === element,
);
if (key) {
newGroupBy.push(key);
}
}
}
setGroupBy(newGroupBy);
setGroupBySearchValue('');
// Update params
setParams({ groupBy: newGroupBy.map((g) => g.key) });
},
[groupByFiltersData, setGroupBy, allAvailableGroupByOptions, setParams],
);
// Sync params to local groupBy state on param change
useEffect(() => {
if (
params.groupBy &&
Array.isArray(params.groupBy) &&
!isEqual(
params.groupBy,
groupBy.map((g) => g.key),
)
) {
// Only update if different
const newGroupBy = params.groupBy
.map((key) => allAvailableGroupByOptions[key])
.filter(Boolean);
if (newGroupBy.length === params.groupBy.length) {
setGroupBy(newGroupBy);
}
}
}, [params.groupBy, allAvailableGroupByOptions, groupBy, setGroupBy]);
useEffect(() => {
if (groupByFiltersData?.payload) {
// Update dropdown options
setGroupByOptions(
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
value: filter.key,
label: filter.key,
})) || [],
);
// Cache all available options to preserve selected values using functional update
setAllAvailableGroupByOptions((prevOptions) => {
const newOptions = { ...prevOptions };
groupByFiltersData?.payload?.attributeKeys?.forEach((filter) => {
newOptions[filter.key] = filter;
});
return newOptions;
});
}
}, [groupByFiltersData]); // Only depends on groupByFiltersData now
// Cache existing selected options on component mount
useEffect(() => {
if (groupBy && groupBy.length > 0) {
setAllAvailableGroupByOptions((prevOptions) => {
const newOptions = { ...prevOptions };
groupBy.forEach((option) => {
newOptions[option.key] = option;
});
return newOptions;
});
}
}, [groupBy]);
const onRowClick = useCallback(
(props: any): void => {
setSelectedEndPointName(props[SPAN_ATTRIBUTES.URL_PATH] as string);
@ -172,6 +211,14 @@ function AllEndPoints({
items: initialItems,
op: 'AND',
});
setParams({
selectedEndPointName: props[SPAN_ATTRIBUTES.URL_PATH] as string,
selectedView: VIEWS.ENDPOINT_STATS,
endPointDetailsLocalFilters: {
items: initialItems,
op: 'AND',
},
});
},
[
filters,
@ -179,6 +226,7 @@ function AllEndPoints({
setSelectedEndPointName,
setSelectedView,
groupBy,
setParams,
],
);

View File

@ -11,12 +11,13 @@ import {
import { useIsDarkMode } from 'hooks/useDarkMode';
import GetMinMax from 'lib/getMinMax';
import { ArrowDown, ArrowUp, X } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { useApiMonitoringParams } from '../../../queryParams';
import AllEndPoints from './AllEndPoints';
import DomainMetrics from './components/DomainMetrics';
import { VIEW_TYPES, VIEWS } from './constants';
@ -40,8 +41,13 @@ function DomainDetails({
domainListLength: number;
domainListFilters: IBuilderQuery['filters'];
}): JSX.Element {
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.ALL_ENDPOINTS);
const [selectedEndPointName, setSelectedEndPointName] = useState<string>('');
const [params, setParams] = useApiMonitoringParams();
const [selectedView, setSelectedView] = useState<VIEWS>(
(params.selectedView as VIEWS) || VIEWS.ALL_ENDPOINTS,
);
const [selectedEndPointName, setSelectedEndPointName] = useState<string>(
params.selectedEndPointName || '',
);
const [endPointsGroupBy, setEndPointsGroupBy] = useState<
IBuilderQuery['groupBy']
>([]);
@ -52,8 +58,27 @@ function DomainDetails({
const handleTabChange = (e: RadioChangeEvent): void => {
setSelectedView(e.target.value);
setParams({ selectedView: e.target.value });
};
const handleEndPointChange = (name: string): void => {
setSelectedEndPointName(name);
setParams({ selectedEndPointName: name });
};
useEffect(() => {
if (params.selectedView && params.selectedView !== selectedView) {
setSelectedView(params.selectedView as VIEWS);
}
if (
params.selectedEndPointName !== undefined &&
params.selectedEndPointName !== selectedEndPointName
) {
setSelectedEndPointName(params.selectedEndPointName);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params.selectedView, params.selectedEndPointName]);
const { maxTime, minTime, selectedTime } = useSelector<
AppState,
GlobalReducer
@ -67,34 +92,62 @@ function DomainDetails({
]);
const [selectedInterval, setSelectedInterval] = useState<Time>(
selectedTime as Time,
(params.selectedInterval as Time) || (selectedTime as Time),
);
const [modalTimeRange, setModalTimeRange] = useState(() => ({
startTime: startMs,
endTime: endMs,
}));
// Sync params to local selectedInterval state on param change
useEffect(() => {
if (params.selectedInterval && params.selectedInterval !== selectedInterval) {
setSelectedInterval(params.selectedInterval as Time);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params.selectedInterval]);
const [modalTimeRange, setModalTimeRange] = useState(() => {
if (params.modalTimeRange) {
return params.modalTimeRange;
}
return {
startTime: startMs,
endTime: endMs,
};
});
// Sync params to local modalTimeRange state on param change
useEffect(() => {
if (
params.modalTimeRange &&
JSON.stringify(params.modalTimeRange) !== JSON.stringify(modalTimeRange)
) {
setModalTimeRange(params.modalTimeRange);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params.modalTimeRange]);
const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
setSelectedInterval(interval as Time);
setParams({ selectedInterval: interval as string });
if (interval === 'custom' && dateTimeRange) {
setModalTimeRange({
const newRange = {
startTime: Math.floor(dateTimeRange[0] / 1000),
endTime: Math.floor(dateTimeRange[1] / 1000),
});
};
setModalTimeRange(newRange);
setParams({ modalTimeRange: newRange });
} else {
const { maxTime, minTime } = GetMinMax(interval);
setModalTimeRange({
const newRange = {
startTime: Math.floor(minTime / TimeRangeOffset),
endTime: Math.floor(maxTime / TimeRangeOffset),
});
};
setModalTimeRange(newRange);
setParams({ modalTimeRange: newRange });
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
[setParams],
);
return (
@ -174,7 +227,6 @@ function DomainDetails({
>
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === VIEW_TYPES.ALL_ENDPOINTS ? 'selected_view tab' : 'tab'
}
value={VIEW_TYPES.ALL_ENDPOINTS}
@ -204,7 +256,7 @@ function DomainDetails({
{selectedView === VIEW_TYPES.ALL_ENDPOINTS && (
<AllEndPoints
domainName={domainData.domainName}
setSelectedEndPointName={setSelectedEndPointName}
setSelectedEndPointName={handleEndPointChange}
setSelectedView={setSelectedView}
groupBy={endPointsGroupBy}
setGroupBy={setEndPointsGroupBy}
@ -218,7 +270,7 @@ function DomainDetails({
<EndPointDetails
domainName={domainData.domainName}
endPointName={selectedEndPointName}
setSelectedEndPointName={setSelectedEndPointName}
setSelectedEndPointName={handleEndPointChange}
initialFilters={initialFiltersEndPointStats}
timeRange={modalTimeRange}
handleTimeChange={handleTimeChange}

View File

@ -1,5 +1,6 @@
import { ENTITY_VERSION_V4 } from 'constants/app';
import { initialQueriesMap } from 'constants/queryBuilder';
import { useApiMonitoringParams } from 'container/ApiMonitoring/queryParams';
import {
END_POINT_DETAILS_QUERY_KEYS_ARRAY,
extractPortAndEndpoint,
@ -59,13 +60,16 @@ function EndPointDetails({
) => void;
}): JSX.Element {
const { startTime: minTime, endTime: maxTime } = timeRange;
const [params, setParams] = useApiMonitoringParams();
const currentQuery = initialQueriesMap[DataSource.TRACES];
// Local state for filters, combining endpoint filter and search filters
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
// Initialize filters based on the initial endPointName prop
const initialItems = [...initialFilters.items];
const initialItems = params.endPointDetailsLocalFilters
? [...params.endPointDetailsLocalFilters.items]
: [...initialFilters.items];
if (endPointName) {
initialItems.push({
id: '92b8a1c1',
@ -107,11 +111,26 @@ function EndPointDetails({
});
}, [endPointName]);
// Separate effect to update params when filters change
useEffect(() => {
const filtersWithoutHttpUrl = {
op: 'AND',
items: filters.items.filter((item) => item.key?.key !== httpUrlKey.key),
};
setParams({ endPointDetailsLocalFilters: filtersWithoutHttpUrl });
}, [filters, setParams]);
// Handler for changes from the QueryBuilderSearchV2 component
const handleFilterChange = useCallback(
(newFilters: IBuilderQuery['filters']): void => {
// 1. Update local filters state immediately
setFilters(newFilters);
// Filter out http.url filter before saving to params
const filteredNewFilters = {
op: 'AND',
items: newFilters.items.filter((item) => item.key?.key !== httpUrlKey.key),
};
setParams({ endPointDetailsLocalFilters: filteredNewFilters });
// 2. Derive the endpoint name from the *new* filters state
const httpUrlFilter = newFilters.items.find(
@ -126,7 +145,7 @@ function EndPointDetails({
setSelectedEndPointName(derivedEndPointName);
}
},
[endPointName, setSelectedEndPointName], // Dependencies for the callback
[endPointName, setSelectedEndPointName, setParams], // Dependencies for the callback
);
const updatedCurrentQuery = useMemo(

View File

@ -215,7 +215,9 @@ function TopErrors({
/>
<Typography.Text className="no-filtered-endpoints-message">
This query had no results. Edit your query and try again!
{showStatusCodeErrors
? 'Please disable "Status Message Exists" toggle to see all errors'
: 'This query had no results. Edit your query and try again!'}
</Typography.Text>
</div>
</div>

View File

@ -21,6 +21,7 @@ function MetricOverTimeGraph({
onDragSelect={onDragSelect}
customOnDragSelect={(): void => {}}
customTimeRange={timeRange}
customTimeRangeWindowForCoRelation="5m"
/>
</div>
</Card>

View File

@ -19,6 +19,7 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { useNotifications } from 'hooks/useNotifications';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useCallback, useMemo, useRef, useState } from 'react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
@ -150,7 +151,12 @@ function StatusCodeBarCharts({
metric?: { [key: string]: string },
queryData?: { queryName: string; inFocusOrNot: boolean },
): void => {
const TWO_AND_HALF_MINUTES_IN_MILLISECONDS = 2.5 * 60 * 1000; // 150,000 milliseconds
const customFilters = getCustomFiltersForBarChart(metric);
const { start, end } = getStartAndEndTimesInMilliseconds(
xValue,
TWO_AND_HALF_MINUTES_IN_MILLISECONDS,
);
handleGraphClick({
xValue,
yValue,
@ -164,6 +170,10 @@ function StatusCodeBarCharts({
notifications,
graphClick,
customFilters,
customTracesTimeRange: {
start,
end,
},
});
},
[

View File

@ -16,7 +16,7 @@ import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQue
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@ -25,6 +25,7 @@ import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { DEFAULT_PARAMS, useApiMonitoringParams } from '../../queryParams';
import {
columnsConfig,
formatDataForTable,
@ -32,7 +33,9 @@ import {
} from '../../utils';
import DomainDetails from './DomainDetails/DomainDetails';
function DomainList({ showIP }: { showIP: boolean }): JSX.Element {
function DomainList(): JSX.Element {
const [params, setParams] = useApiMonitoringParams();
const { showIP, selectedDomain } = params;
const [selectedDomainIndex, setSelectedDomainIndex] = useState<number>(-1);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
@ -129,6 +132,16 @@ function DomainList({ showIP }: { showIP: boolean }): JSX.Element {
[data],
);
// Open drawer if selectedDomain is set in URL
useEffect(() => {
if (selectedDomain && formattedDataForTable?.length > 0) {
const idx = formattedDataForTable.findIndex(
(item) => item.domainName === selectedDomain,
);
setSelectedDomainIndex(idx);
}
}, [selectedDomain, formattedDataForTable]);
return (
<section className={cx('api-module-right-section')}>
<Toolbar
@ -179,6 +192,7 @@ function DomainList({ showIP }: { showIP: boolean }): JSX.Element {
(item) => item.key === record.key,
);
setSelectedDomainIndex(dataIndex);
setParams({ selectedDomain: record.domainName });
logEvent('API Monitoring: Domain name row clicked', {});
}
},
@ -196,6 +210,7 @@ function DomainList({ showIP }: { showIP: boolean }): JSX.Element {
domainListLength={formattedDataForTable.length}
handleClose={(): void => {
setSelectedDomainIndex(-1);
setParams(DEFAULT_PARAMS);
}}
domainListFilters={query?.filters}
/>

View File

@ -8,13 +8,15 @@ import cx from 'classnames';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { useApiMonitoringParams } from '../queryParams';
import { ApiMonitoringQuickFiltersConfig } from '../utils';
import DomainList from './Domains/DomainList';
function Explorer(): JSX.Element {
const [showIP, setShowIP] = useState<boolean>(true);
const [params, setParams] = useApiMonitoringParams();
const showIP = params.showIP ?? true;
useEffect(() => {
logEvent('API Monitoring: Landing page visited', {});
@ -34,14 +36,12 @@ function Explorer(): JSX.Element {
<Switch
size="small"
style={{ marginLeft: 'auto' }}
checked={showIP}
checked={showIP ?? true}
onClick={(): void => {
setShowIP((showIP): boolean => {
logEvent('API Monitoring: Show IP addresses clicked', {
showIP: !showIP,
});
return !showIP;
logEvent('API Monitoring: Show IP addresses clicked', {
showIP: !(showIP ?? true),
});
setParams({ showIP });
}}
/>
</div>
@ -52,7 +52,7 @@ function Explorer(): JSX.Element {
handleFilterVisibilityChange={(): void => {}}
/>
</section>
<DomainList showIP={showIP} />
<DomainList />
</div>
</Sentry.ErrorBoundary>
);

View File

@ -79,6 +79,24 @@ jest.mock('antd', () => {
};
});
// Mock useApiMonitoringParams hook
jest.mock('container/ApiMonitoring/queryParams', () => ({
useApiMonitoringParams: jest.fn().mockReturnValue([
{
showIP: true,
selectedDomain: '',
selectedView: 'all_endpoints',
selectedEndPointName: '',
groupBy: [],
allEndpointsLocalFilters: undefined,
endPointDetailsLocalFilters: undefined,
modalTimeRange: undefined,
selectedInterval: undefined,
},
jest.fn(),
]),
}));
describe('AllEndPoints', () => {
const mockProps = {
domainName: 'test-domain',

View File

@ -25,6 +25,24 @@ jest.mock('react-query', () => ({
useQueries: jest.fn(),
}));
// Mock useApiMonitoringParams hook
jest.mock('container/ApiMonitoring/queryParams', () => ({
useApiMonitoringParams: jest.fn().mockReturnValue([
{
showIP: true,
selectedDomain: '',
selectedView: 'all_endpoints',
selectedEndPointName: '',
groupBy: [],
allEndpointsLocalFilters: undefined,
endPointDetailsLocalFilters: undefined,
modalTimeRange: undefined,
selectedInterval: undefined,
},
jest.fn(),
]),
}));
jest.mock('container/ApiMonitoring/utils', () => ({
END_POINT_DETAILS_QUERY_KEYS_ARRAY: [
'endPointMetricsData',

View File

@ -0,0 +1,276 @@
/* eslint-disable sonarjs/no-duplicate-string */
import {
ApiMonitoringParams,
DEFAULT_PARAMS,
getApiMonitoringParams,
setApiMonitoringParams,
} from 'container/ApiMonitoring/queryParams';
// Mock react-router-dom hooks
jest.mock('react-router-dom', () => {
const originalModule = jest.requireActual('react-router-dom');
return {
...originalModule,
useLocation: jest.fn(),
useHistory: jest.fn(),
};
});
describe('API Monitoring Query Params', () => {
describe('getApiMonitoringParams', () => {
it('returns default params when no query param exists', () => {
const search = '';
expect(getApiMonitoringParams(search)).toEqual(DEFAULT_PARAMS);
});
it('parses URL query params correctly', () => {
const mockParams: Partial<ApiMonitoringParams> = {
showIP: false,
selectedDomain: 'test-domain',
selectedView: 'test-view',
selectedEndPointName: '/api/test',
};
// Create a URL search string with encoded params
const urlParams = new URLSearchParams();
urlParams.set(
'apiMonitoringParams',
encodeURIComponent(JSON.stringify(mockParams)),
);
const search = `?${urlParams.toString()}`;
const result = getApiMonitoringParams(search);
// Only check specific properties that we set, not all DEFAULT_PARAMS
expect(result.showIP).toBe(mockParams.showIP);
expect(result.selectedDomain).toBe(mockParams.selectedDomain);
expect(result.selectedView).toBe(mockParams.selectedView);
expect(result.selectedEndPointName).toBe(mockParams.selectedEndPointName);
});
it('returns default params when parsing fails', () => {
const urlParams = new URLSearchParams();
urlParams.set('apiMonitoringParams', 'invalid-json');
const search = `?${urlParams.toString()}`;
expect(getApiMonitoringParams(search)).toEqual(DEFAULT_PARAMS);
});
});
describe('setApiMonitoringParams', () => {
it('updates URL with new params (push mode)', () => {
const history = {
push: jest.fn(),
replace: jest.fn(),
};
const search = '';
const newParams: Partial<ApiMonitoringParams> = {
showIP: false,
selectedDomain: 'updated-domain',
};
setApiMonitoringParams(newParams, search, history as any, false);
expect(history.push).toHaveBeenCalledWith({
search: expect.stringContaining('apiMonitoringParams'),
});
expect(history.replace).not.toHaveBeenCalled();
// Verify that the search string contains the expected encoded params
const searchArg = history.push.mock.calls[0][0].search;
const params = new URLSearchParams(searchArg);
const decoded = JSON.parse(
decodeURIComponent(params.get('apiMonitoringParams') || ''),
);
// Test only the specific fields we set
expect(decoded.showIP).toBe(newParams.showIP);
expect(decoded.selectedDomain).toBe(newParams.selectedDomain);
});
it('updates URL with new params (replace mode)', () => {
const history = {
push: jest.fn(),
replace: jest.fn(),
};
const search = '';
const newParams: Partial<ApiMonitoringParams> = {
showIP: false,
selectedDomain: 'updated-domain',
};
setApiMonitoringParams(newParams, search, history as any, true);
expect(history.replace).toHaveBeenCalledWith({
search: expect.stringContaining('apiMonitoringParams'),
});
expect(history.push).not.toHaveBeenCalled();
});
it('merges new params with existing params', () => {
const history = {
push: jest.fn(),
replace: jest.fn(),
};
// Start with some existing params
const existingParams: Partial<ApiMonitoringParams> = {
showIP: true,
selectedDomain: 'domain-1',
selectedView: 'view-1',
};
const urlParams = new URLSearchParams();
urlParams.set(
'apiMonitoringParams',
encodeURIComponent(JSON.stringify(existingParams)),
);
const search = `?${urlParams.toString()}`;
// Add some new params
const newParams: Partial<ApiMonitoringParams> = {
selectedDomain: 'domain-2',
selectedEndPointName: '/api/test',
};
setApiMonitoringParams(newParams, search, history as any, false);
// Verify merged params
const searchArg = history.push.mock.calls[0][0].search;
const params = new URLSearchParams(searchArg);
const decoded = JSON.parse(
decodeURIComponent(params.get('apiMonitoringParams') || ''),
);
// Test only the specific fields
expect(decoded.showIP).toBe(existingParams.showIP);
expect(decoded.selectedView).toBe(existingParams.selectedView);
expect(decoded.selectedDomain).toBe(newParams.selectedDomain); // This should be overwritten
expect(decoded.selectedEndPointName).toBe(newParams.selectedEndPointName);
});
});
describe('useApiMonitoringParams hook without calling hook directly', () => {
// Instead of using the hook directly, We are testing the individual functions that make up the hook
// as the original hook contains react core hooks
const mockUseLocationAndHistory = (initialSearch = ''): any => {
// Create mock location object
const location = {
search: initialSearch,
pathname: '/some-path',
hash: '',
state: null,
};
// Create mock history object
const history = {
push: jest.fn((args) => {
// Simulate updating the location search
location.search = args.search;
}),
replace: jest.fn((args) => {
location.search = args.search;
}),
length: 1,
location,
};
// Set up mocks for useLocation and useHistory
const useLocationMock = jest.requireMock('react-router-dom').useLocation;
const useHistoryMock = jest.requireMock('react-router-dom').useHistory;
useLocationMock.mockReturnValue(location);
useHistoryMock.mockReturnValue(history);
return { location, history };
};
it('retrieves URL params correctly from location', () => {
const testParams: Partial<ApiMonitoringParams> = {
showIP: false,
selectedDomain: 'test-domain',
selectedView: 'custom-view',
};
const urlParams = new URLSearchParams();
urlParams.set(
'apiMonitoringParams',
encodeURIComponent(JSON.stringify(testParams)),
);
const search = `?${urlParams.toString()}`;
const result = getApiMonitoringParams(search);
// Test only specific fields
expect(result.showIP).toBe(testParams.showIP);
expect(result.selectedDomain).toBe(testParams.selectedDomain);
expect(result.selectedView).toBe(testParams.selectedView);
});
it('updates URL correctly with new params', () => {
const { location, history } = mockUseLocationAndHistory();
const newParams: Partial<ApiMonitoringParams> = {
selectedDomain: 'new-domain',
showIP: false,
};
// Manually execute the core logic of the hook's setParams function
setApiMonitoringParams(newParams, location.search, history as any);
expect(history.push).toHaveBeenCalledWith({
search: expect.stringContaining('apiMonitoringParams'),
});
const searchArg = history.push.mock.calls[0][0].search;
const params = new URLSearchParams(searchArg);
const decoded = JSON.parse(
decodeURIComponent(params.get('apiMonitoringParams') || ''),
);
// Test only specific fields
expect(decoded.selectedDomain).toBe(newParams.selectedDomain);
expect(decoded.showIP).toBe(newParams.showIP);
});
it('preserves existing params when updating', () => {
// Create a search string with existing params
const initialParams: Partial<ApiMonitoringParams> = {
showIP: false,
selectedDomain: 'initial-domain',
};
const urlParams = new URLSearchParams();
urlParams.set(
'apiMonitoringParams',
encodeURIComponent(JSON.stringify(initialParams)),
);
const initialSearch = `?${urlParams.toString()}`;
// Set up mocks
const { location, history } = mockUseLocationAndHistory(initialSearch);
// Manually execute the core logic
setApiMonitoringParams(
{ selectedView: 'new-view' },
location.search,
history as any,
);
// Verify history was updated
expect(history.push).toHaveBeenCalled();
// Parse the new query params from the URL
const searchArg = history.push.mock.calls[0][0].search;
const params = new URLSearchParams(searchArg);
const decoded = JSON.parse(
decodeURIComponent(params.get('apiMonitoringParams') || ''),
);
// Test only specific fields
expect(decoded.showIP).toBe(initialParams.showIP);
expect(decoded.selectedDomain).toBe(initialParams.selectedDomain);
expect(decoded.selectedView).toBe('new-view');
});
});
});

View File

@ -0,0 +1,88 @@
import { useCallback } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
// --- Types for all API Monitoring query params ---
export interface ApiMonitoringParams {
showIP?: boolean;
selectedDomain?: string;
selectedView?: string;
selectedEndPointName?: string;
groupBy?: string[];
allEndpointsLocalFilters?: any;
endPointDetailsLocalFilters?: any;
modalTimeRange?: { startTime: number; endTime: number };
selectedInterval?: string;
// Add more params as needed
}
export const DEFAULT_PARAMS: ApiMonitoringParams = {
showIP: true,
selectedDomain: '',
selectedView: 'all_endpoints',
selectedEndPointName: '',
groupBy: [],
allEndpointsLocalFilters: undefined,
endPointDetailsLocalFilters: undefined,
modalTimeRange: undefined,
selectedInterval: undefined,
};
const PARAM_KEY = 'apiMonitoringParams';
// --- Parse and serialize helpers ---
function encodeParams(params: ApiMonitoringParams): string {
return encodeURIComponent(JSON.stringify(params));
}
function decodeParams(value: string | null): ApiMonitoringParams {
if (!value) return DEFAULT_PARAMS;
try {
return JSON.parse(decodeURIComponent(value));
} catch {
return DEFAULT_PARAMS;
}
}
// --- Read query params from URL ---
export function getApiMonitoringParams(search: string): ApiMonitoringParams {
const params = new URLSearchParams(search);
return decodeParams(params.get(PARAM_KEY));
}
// --- Set query params in URL (replace or push) ---
export function setApiMonitoringParams(
newParams: Partial<ApiMonitoringParams>,
search: string,
history: ReturnType<typeof useHistory>,
replace = false,
): void {
const urlParams = new URLSearchParams(search);
const current = decodeParams(urlParams.get(PARAM_KEY));
const merged = { ...current, ...newParams };
urlParams.set(PARAM_KEY, encodeParams(merged));
const newSearch = `?${urlParams.toString()}`;
if (replace) {
history.replace({ search: newSearch });
} else {
history.push({ search: newSearch });
}
}
// --- React hook to use query params in a component ---
export function useApiMonitoringParams(): [
ApiMonitoringParams,
(newParams: Partial<ApiMonitoringParams>, replace?: boolean) => void,
] {
const location = useLocation();
const history = useHistory();
const params = getApiMonitoringParams(location.search);
const setParams = useCallback(
(newParams: Partial<ApiMonitoringParams>, replace = false) => {
setApiMonitoringParams(newParams, location.search, history, replace);
},
[location.search, history],
);
return [params, setParams];
}

View File

@ -3796,9 +3796,9 @@ export const getRateOverTimeWidgetData = (
): Widgets => {
let legend = domainName;
if (endPointName) {
const { endpoint, port } = extractPortAndEndpoint(endPointName);
const { endpoint } = extractPortAndEndpoint(endPointName);
// eslint-disable-next-line sonarjs/no-nested-template-literals
legend = `${port !== '-' && port !== 'n/a' ? `${port}:` : ''}${endpoint}`;
legend = `${endpoint}`;
}
return getWidgetQueryBuilder(
@ -3861,9 +3861,9 @@ export const getLatencyOverTimeWidgetData = (
): Widgets => {
let legend = domainName;
if (endPointName) {
const { endpoint, port } = extractPortAndEndpoint(endPointName);
const { endpoint } = extractPortAndEndpoint(endPointName);
// eslint-disable-next-line sonarjs/no-nested-template-literals
legend = `${port !== '-' && port !== 'n/a' ? `${port}:` : ''}${endpoint}`;
legend = `${endpoint}`;
}
return getWidgetQueryBuilder(

View File

@ -15,6 +15,10 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import {
getCustomTimeRangeWindowSweepInMS,
getStartAndEndTimesInMilliseconds,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
Dispatch,
@ -57,6 +61,7 @@ function WidgetGraphComponent({
customSeries,
customErrorMessage,
customOnRowClick,
customTimeRangeWindowForCoRelation,
}: WidgetGraphComponentProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const [deleteModal, setDeleteModal] = useState(false);
@ -263,6 +268,13 @@ function WidgetGraphComponent({
metric?: { [key: string]: string },
queryData?: { queryName: string; inFocusOrNot: boolean },
): void => {
const customTracesTimeRange = getCustomTimeRangeWindowSweepInMS(
customTimeRangeWindowForCoRelation,
);
const { start, end } = getStartAndEndTimesInMilliseconds(
xValue,
customTracesTimeRange,
);
handleGraphClick({
xValue,
yValue,
@ -275,6 +287,9 @@ function WidgetGraphComponent({
navigateToExplorer,
notifications,
graphClick,
...(customTimeRangeWindowForCoRelation
? { customTracesTimeRange: { start, end } }
: {}),
});
};
@ -393,6 +408,7 @@ WidgetGraphComponent.defaultProps = {
yAxisUnit: undefined,
setLayout: undefined,
onClickHandler: undefined,
customTimeRangeWindowForCoRelation: undefined,
};
export default WidgetGraphComponent;

View File

@ -49,6 +49,7 @@ function GridCardGraph({
analyticsEvent,
customTimeRange,
customOnRowClick,
customTimeRangeWindowForCoRelation,
}: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>();
@ -289,6 +290,7 @@ function GridCardGraph({
customSeries={customSeries}
customErrorMessage={isInternalServerError ? customErrorMessage : undefined}
customOnRowClick={customOnRowClick}
customTimeRangeWindowForCoRelation={customTimeRangeWindowForCoRelation}
/>
)}
</div>
@ -303,6 +305,7 @@ GridCardGraph.defaultProps = {
headerMenuList: [MenuItemKeys.View],
version: 'v3',
analyticsEvent: undefined,
customTimeRangeWindowForCoRelation: undefined,
};
export default memo(GridCardGraph);

View File

@ -40,6 +40,7 @@ export interface WidgetGraphComponentProps {
customSeries?: (data: QueryData[]) => uPlot.Series[];
customErrorMessage?: string;
customOnRowClick?: (record: RowData) => void;
customTimeRangeWindowForCoRelation?: string | undefined;
}
export interface GridCardGraphProps {
@ -67,6 +68,7 @@ export interface GridCardGraphProps {
endTime: number;
};
customOnRowClick?: (record: RowData) => void;
customTimeRangeWindowForCoRelation?: string | undefined;
}
export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@ -179,6 +179,7 @@ interface HandleGraphClickParams {
notifications: NotificationInstance;
graphClick: (props: GraphClickProps) => void;
customFilters?: TagFilterItem[];
customTracesTimeRange?: { start: number; end: number };
}
export const handleGraphClick = async ({
@ -194,6 +195,7 @@ export const handleGraphClick = async ({
notifications,
graphClick,
customFilters,
customTracesTimeRange,
}: HandleGraphClickParams): Promise<void> => {
const { stepInterval } = widget?.query?.builder?.queryData?.[0] ?? {};
@ -225,8 +227,10 @@ export const handleGraphClick = async ({
navigateToExplorer({
filters: [...result[key].filters, ...(customFilters || [])],
dataSource: result[key].dataSource as DataSource,
startTime: xValue,
endTime: xValue + (stepInterval ?? 60),
startTime: customTracesTimeRange ? customTracesTimeRange?.start : xValue,
endTime: customTracesTimeRange
? customTracesTimeRange?.end
: xValue + (stepInterval ?? 60),
shouldResolveQuery: true,
}),
}));

View File

@ -31,10 +31,11 @@ import { v4 as uuid } from 'uuid';
import { GraphTitle, legend, MENU_ITEMS } from '../constant';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import { Card, GraphContainer, Row } from '../styles';
import { Button } from './styles';
import GraphControlsPanel from './Overview/GraphControlsPanel/GraphControlsPanel';
import { IServiceName } from './types';
import {
handleNonInQueryRange,
onViewAPIMonitoringPopupClick,
onViewTracePopupClick,
useGetAPMToTracesQueries,
useGraphClickHandler,
@ -42,7 +43,7 @@ import {
function External(): JSX.Element {
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
const [selectedData, setSelectedData] = useState<any>(undefined);
const { servicename: encodedServiceName } = useParams<IServiceName>();
const servicename = decodeURIComponent(encodedServiceName);
@ -223,17 +224,18 @@ function External(): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const onGraphClickHandler = useGraphClickHandler(setSelectedTimeStamp);
const onGraphClickHandler = useGraphClickHandler(
setSelectedTimeStamp,
setSelectedData,
);
return (
<>
<Row gutter={24}>
<Col span={12}>
<Button
type="default"
size="small"
<GraphControlsPanel
id="external_call_error_percentage_button"
onClick={onViewTracePopupClick({
onViewTracesClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
@ -241,21 +243,28 @@ function External(): JSX.Element {
stepInterval,
safeNavigate,
})}
>
View Traces
</Button>
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address || '',
isError: true,
stepInterval: 300,
safeNavigate,
})}
/>
<Card data-testid="external_call_error_percentage">
<GraphContainer>
<Graph
headerMenuList={MENU_ITEMS}
widget={externalCallErrorWidget}
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
onClickHandler={(xValue, yValue, mouseX, mouseY, data): void => {
onGraphClickHandler(
xValue,
yValue,
mouseX,
mouseY,
'external_call_error_percentage',
data,
);
}}
onDragSelect={onDragSelect}
@ -266,11 +275,9 @@ function External(): JSX.Element {
</Col>
<Col span={12}>
<Button
type="default"
size="small"
<GraphControlsPanel
id="external_call_duration_button"
onClick={onViewTracePopupClick({
onViewTracesClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
@ -278,22 +285,29 @@ function External(): JSX.Element {
stepInterval,
safeNavigate,
})}
>
View Traces
</Button>
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address,
isError: false,
stepInterval: 300,
safeNavigate,
})}
/>
<Card data-testid="external_call_duration">
<GraphContainer>
<Graph
headerMenuList={MENU_ITEMS}
widget={externalCallDurationWidget}
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
onClickHandler={(xValue, yValue, mouseX, mouseY, data): void => {
onGraphClickHandler(
xValue,
yValue,
mouseX,
mouseY,
'external_call_duration',
data,
);
}}
onDragSelect={onDragSelect}
@ -306,11 +320,9 @@ function External(): JSX.Element {
<Row gutter={24}>
<Col span={12}>
<Button
type="default"
size="small"
<GraphControlsPanel
id="external_call_rps_by_address_button"
onClick={onViewTracePopupClick({
onViewTracesClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
@ -318,21 +330,28 @@ function External(): JSX.Element {
stepInterval,
safeNavigate,
})}
>
View Traces
</Button>
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address,
isError: false,
stepInterval: 300,
safeNavigate,
})}
/>
<Card data-testid="external_call_rps_by_address">
<GraphContainer>
<Graph
widget={externalCallRPSWidget}
headerMenuList={MENU_ITEMS}
onClickHandler={(xValue, yValue, mouseX, mouseY): Promise<void> =>
onClickHandler={(xValue, yValue, mouseX, mouseY, data): Promise<void> =>
onGraphClickHandler(
xValue,
yValue,
mouseX,
mouseY,
'external_call_rps_by_address',
data,
)
}
onDragSelect={onDragSelect}
@ -343,11 +362,9 @@ function External(): JSX.Element {
</Col>
<Col span={12}>
<Button
type="default"
size="small"
<GraphControlsPanel
id="external_call_duration_by_address_button"
onClick={onViewTracePopupClick({
onViewTracesClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
@ -355,22 +372,29 @@ function External(): JSX.Element {
stepInterval,
safeNavigate,
})}
>
View Traces
</Button>
onViewAPIMonitoringClick={onViewAPIMonitoringPopupClick({
servicename,
timestamp: selectedTimeStamp,
domainName: selectedData?.address,
isError: false,
stepInterval: 300,
safeNavigate,
})}
/>
<Card data-testid="external_call_duration_by_address">
<GraphContainer>
<Graph
widget={externalCallDurationAddressWidget}
headerMenuList={MENU_ITEMS}
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
onClickHandler={(xValue, yValue, mouseX, mouseY, data): void => {
onGraphClickHandler(
xValue,
yValue,
mouseX,
mouseY,
'external_call_duration_by_address',
data,
);
}}
onDragSelect={onDragSelect}

View File

@ -2,7 +2,7 @@
position: absolute;
z-index: 999;
display: none;
width: 110px;
width: 150px;
padding: 5px;
border-radius: 5px;
background: var(--bg-slate-400);

View File

@ -2,18 +2,20 @@ import './GraphControlsPanel.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import { DraftingCompass, ScrollText } from 'lucide-react';
import { Binoculars, DraftingCompass, ScrollText } from 'lucide-react';
interface GraphControlsPanelProps {
id: string;
onViewLogsClick: () => void;
onViewLogsClick?: () => void;
onViewTracesClick: () => void;
onViewAPIMonitoringClick?: () => void;
}
function GraphControlsPanel({
id,
onViewLogsClick,
onViewTracesClick,
onViewAPIMonitoringClick,
}: GraphControlsPanelProps): JSX.Element {
return (
<div id={id} className="graph-controls-panel">
@ -26,17 +28,35 @@ function GraphControlsPanel({
>
View traces
</Button>
<Button
type="link"
icon={<ScrollText size={14} />}
size="small"
onClick={onViewLogsClick}
style={{ color: Color.BG_VANILLA_100 }}
>
View logs
</Button>
{onViewLogsClick && (
<Button
type="link"
icon={<ScrollText size={14} />}
size="small"
onClick={onViewLogsClick}
style={{ color: Color.BG_VANILLA_100 }}
>
View logs
</Button>
)}
{onViewAPIMonitoringClick && (
<Button
type="link"
icon={<Binoculars size={14} />}
size="small"
onClick={onViewAPIMonitoringClick}
style={{ color: Color.BG_VANILLA_100 }}
>
View Third Party API
</Button>
)}
</div>
);
}
GraphControlsPanel.defaultProps = {
onViewLogsClick: undefined,
onViewAPIMonitoringClick: undefined,
};
export default GraphControlsPanel;

View File

@ -7,6 +7,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useClickOutside from 'hooks/useClickOutside';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { resourceAttributesToTracesFilterItems } from 'hooks/useResourceAttribute/utils';
import createQueryParams from 'lib/createQueryParams';
import { prepareQueryWithDefaultTimestamp } from 'pages/LogsExplorer/utils';
import { traceFilterKeys } from 'pages/TracesExplorer/Filter/filterUtils';
import { Dispatch, SetStateAction, useMemo, useRef } from 'react';
@ -14,7 +15,11 @@ import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
Query,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { Tags } from 'types/reducer/trace';
import { secondsToMilliseconds } from 'utils/timeUtils';
@ -40,6 +45,16 @@ interface OnViewTracePopupClickProps {
safeNavigate: (url: string) => void;
}
interface OnViewAPIMonitoringPopupClickProps {
servicename: string;
timestamp: number;
stepInterval?: number;
domainName: string;
isError: boolean;
safeNavigate: (url: string) => void;
}
export function generateExplorerPath(
isViewLogsClicked: boolean | undefined,
urlParams: URLSearchParams,
@ -107,14 +122,92 @@ export function onViewTracePopupClick({
};
}
const generateAPIMonitoringPath = (
domainName: string,
startTime: number,
endTime: number,
filters: IBuilderQuery['filters'],
): string => {
const basePath = ROUTES.API_MONITORING;
return `${basePath}?${createQueryParams({
apiMonitoringParams: JSON.stringify({
selectedDomain: domainName,
selectedView: 'endpoint_stats',
modalTimeRange: {
startTime,
endTime,
},
selectedInterval: 'custom',
endPointDetailsLocalFilters: filters,
}),
})}`;
};
export function onViewAPIMonitoringPopupClick({
servicename,
timestamp,
domainName,
isError,
stepInterval,
safeNavigate,
}: OnViewAPIMonitoringPopupClickProps): VoidFunction {
return (): void => {
const endTime = timestamp + (stepInterval || 60);
const startTime = timestamp - (stepInterval || 60);
const filters = {
items: [
...(isError
? [
{
id: uuid().slice(0, 8),
key: {
key: 'hasError',
dataType: DataTypes.bool,
type: 'tag',
isColumn: true,
isJSON: false,
id: 'hasError--bool--tag--true',
},
op: 'in',
value: ['true'],
},
]
: []),
{
id: uuid().slice(0, 8),
key: {
key: 'service.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
op: '=',
value: servicename,
},
],
op: 'AND',
};
const newPath = generateAPIMonitoringPath(
domainName,
startTime,
endTime,
filters,
);
safeNavigate(newPath);
};
}
export function useGraphClickHandler(
setSelectedTimeStamp: (n: number) => void | Dispatch<SetStateAction<number>>,
setSelectedData?: (data: any) => void | Dispatch<SetStateAction<any>>,
): (
xValue: number,
yValue: number,
mouseX: number,
mouseY: number,
type: string,
data?: any,
) => Promise<void> {
const buttonRef = useRef<HTMLElement | null>(null);
@ -134,6 +227,7 @@ export function useGraphClickHandler(
mouseX: number,
mouseY: number,
type: string,
data?: any,
): Promise<void> => {
const id = `${type}_button`;
const buttonElement = document.getElementById(id);
@ -145,6 +239,9 @@ export function useGraphClickHandler(
buttonElement.style.left = `${mouseX}px`;
buttonElement.style.top = `${mouseY}px`;
setSelectedTimeStamp(Math.floor(xValue));
if (setSelectedData && data) {
setSelectedData(data);
}
}
} else if (buttonElement && buttonElement.style.display === 'block') {
buttonElement.style.display = 'none';

View File

@ -125,7 +125,7 @@ const menuItems: SidebarItem[] = [
},
{
key: ROUTES.API_MONITORING,
label: 'API Monitoring',
label: 'Third Party API',
icon: <Binoculars size={16} />,
isNew: true,
},

View File

@ -175,16 +175,32 @@ export function getWidgetQuery({
export const convertToNanoseconds = (timestamp: number): bigint =>
BigInt((timestamp * 1e9).toFixed(0));
export const getCustomTimeRangeWindowSweepInMS = (
customTimeRangeWindowForCoRelation: string | undefined,
): number => {
switch (customTimeRangeWindowForCoRelation) {
case '5m':
return 2.5 * 60 * 1000;
case '10m':
return 5 * 60 * 1000;
default:
return 5 * 60 * 1000;
}
};
export const getStartAndEndTimesInMilliseconds = (
timestamp: number,
delta?: number,
): { start: number; end: number } => {
const FIVE_MINUTES_IN_MILLISECONDS = 5 * 60 * 1000; // 300,000 milliseconds
const pointInTime = Math.floor(timestamp * 1000);
// Convert timestamp to milliseconds and floor it
const start = Math.floor(pointInTime - FIVE_MINUTES_IN_MILLISECONDS);
const end = Math.floor(pointInTime + FIVE_MINUTES_IN_MILLISECONDS);
const start = Math.floor(
pointInTime - (delta || FIVE_MINUTES_IN_MILLISECONDS),
);
const end = Math.floor(pointInTime + (delta || FIVE_MINUTES_IN_MILLISECONDS));
return { start, end };
};