Compare commits

...

3 Commits

Author SHA1 Message Date
amlannandy
d466134a2e chore: fix CI 2026-02-13 13:25:03 +07:00
amlannandy
e336771e51 chore: fix types 2026-02-13 13:05:08 +07:00
amlannandy
055ebd56d5 chore: metrics explorer v2 api migration in explorer section 2026-02-12 12:28:38 +07:00
10 changed files with 187 additions and 162 deletions

View File

@@ -1,29 +0,0 @@
import { ApiV2Instance as axios } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { MetricMetadataResponse } from 'types/api/metricsExplorer/v2/getMetricMetadata';
export const getMetricMetadata = async (
metricName: string,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponseV2<MetricMetadataResponse> | ErrorResponseV2> => {
try {
const encodedMetricName = encodeURIComponent(metricName);
const response = await axios.get(
`/metrics/metadata?metricName=${encodedMetricName}`,
{
signal,
headers,
},
);
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
return ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};

View File

@@ -1,8 +1,13 @@
import { useMemo } from 'react';
import { useQueries } from 'react-query';
import { useQueries, useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
import { Color } from '@signozhq/design-tokens';
import { Tooltip, Typography } from 'antd';
import { toast } from '@signozhq/sonner';
import { Button, Tooltip, Typography } from 'antd';
import {
invalidateGetMetricMetadata,
useUpdateMetricMetadata,
} from 'api/generated/services/metrics';
import { isAxiosError } from 'axios';
import classNames from 'classnames';
import YAxisUnitSelector from 'components/YAxisUnitSelector';
@@ -23,7 +28,10 @@ import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TimeSeriesProps } from './types';
import { splitQueryIntoOneChartPerQuery } from './utils';
import {
buildUpdateMetricYAxisUnitPayload,
splitQueryIntoOneChartPerQuery,
} from './utils';
function TimeSeries({
showOneChartPerQuery,
@@ -35,6 +43,7 @@ function TimeSeries({
yAxisUnit,
setYAxisUnit,
showYAxisUnitSelector,
metrics,
}: TimeSeriesProps): JSX.Element {
const { stagedQuery, currentQuery } = useQueryBuilder();
@@ -42,6 +51,7 @@ function TimeSeries({
AppState,
GlobalReducer
>((state) => state.globalTime);
const queryClient = useQueryClient();
const isValidToConvertToMs = useMemo(() => {
const isValid: boolean[] = [];
@@ -138,54 +148,51 @@ function TimeSeries({
setYAxisUnit(value);
};
// TODO: Enable once we have resolved all related metrics v2 api issues
// Show the save unit button if
// 1. There is only one metric
// 2. The metric has no saved unit
// 3. The user has selected a unit
// const showSaveUnitButton = useMemo(
// () =>
// metricUnits.length === 1 &&
// Boolean(metrics?.[0]) &&
// !metricUnits[0] &&
// yAxisUnit,
// [metricUnits, metrics, yAxisUnit],
// );
const showSaveUnitButton = useMemo(
() =>
metricUnits.length === 1 &&
Boolean(metrics?.[0]) &&
!metricUnits[0] &&
yAxisUnit,
[metricUnits, metrics, yAxisUnit],
);
// const {
// mutate: updateMetricMetadata,
// isLoading: isUpdatingMetricMetadata,
// } = useUpdateMetricMetadata();
const {
mutate: updateMetricMetadata,
isLoading: isUpdatingMetricMetadata,
} = useUpdateMetricMetadata();
// const handleSaveUnit = (): void => {
// updateMetricMetadata(
// {
// metricName: metricNames[0],
// payload: {
// unit: yAxisUnit,
// description: metrics[0]?.description ?? '',
// metricType: metrics[0]?.type as MetricType,
// temporality: metrics[0]?.temporality,
// },
// },
// {
// onSuccess: () => {
// notifications.success({
// message: 'Unit saved successfully',
// });
// queryClient.invalidateQueries([
// REACT_QUERY_KEY.GET_METRIC_DETAILS,
// metricNames[0],
// ]);
// },
// onError: () => {
// notifications.error({
// message: 'Failed to save unit',
// });
// },
// },
// );
// };
const handleSaveUnit = (): void => {
if (metrics?.[0]) {
updateMetricMetadata(
{
pathParams: {
metricName: metricNames[0],
},
data: buildUpdateMetricYAxisUnitPayload(
metricNames[0],
metrics[0],
yAxisUnit,
),
},
{
onSuccess: () => {
toast.success('Unit saved successfully');
invalidateGetMetricMetadata(queryClient, {
metricName: metricNames[0],
});
},
onError: () => {
toast.error('Failed to save unit');
},
},
);
}
};
return (
<>
@@ -198,8 +205,7 @@ function TimeSeries({
source={YAxisSource.EXPLORER}
data-testid="y-axis-unit-selector"
/>
{/* TODO: Enable once we have resolved all related metrics v2 api issues */}
{/* {showSaveUnitButton && (
{showSaveUnitButton && (
<div className="save-unit-container">
<Typography.Text>
Save the selected unit for this metric?
@@ -213,7 +219,7 @@ function TimeSeries({
<Typography.Paragraph>Yes</Typography.Paragraph>
</Button>
</div>
)} */}
)}
</>
)}
</div>

View File

@@ -3,8 +3,11 @@ import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { render, screen } from '@testing-library/react';
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import {
MetricsexplorertypesMetricMetadataDTO,
MetrictypesTemporalityDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import * as useOptionsMenuHooks from 'container/OptionsMenu';
import * as useUpdateDashboardHooks from 'hooks/dashboard/useUpdateDashboard';
@@ -14,7 +17,6 @@ import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import * as timezoneHooks from 'providers/Timezone';
import store from 'store';
import { LicenseEvent } from 'types/api/licensesV3/getActive';
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
@@ -135,11 +137,11 @@ jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
const Y_AXIS_UNIT_SELECTOR_TEST_ID = 'y-axis-unit-selector';
const mockMetric: MetricMetadata = {
type: MetricType.SUM,
const mockMetric: MetricsexplorertypesMetricMetadataDTO = {
type: MetrictypesTypeDTO.sum,
description: 'metric1 description',
unit: 'metric1 unit',
temporality: Temporality.CUMULATIVE,
temporality: MetrictypesTemporalityDTO.cumulative,
isMonotonic: true,
};
@@ -269,10 +271,10 @@ describe('Explorer', () => {
isError: false,
metrics: [
{
type: MetricType.SUM,
type: MetrictypesTypeDTO.sum,
description: 'metric1 description',
unit: '',
temporality: Temporality.CUMULATIVE,
temporality: MetrictypesTemporalityDTO.cumulative,
isMonotonic: true,
},
],

View File

@@ -1,29 +1,23 @@
import { UseMutationResult } from 'react-query';
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { UpdateMetricMetadataResponse } from 'api/metricsExplorer/updateMetricMetadata';
import * as useUpdateMetricMetadataHooks from 'hooks/metricsExplorer/useUpdateMetricMetadata';
import { UseUpdateMetricMetadataProps } from 'hooks/metricsExplorer/useUpdateMetricMetadata';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
import * as metricsExplorerHooks from 'api/generated/services/metrics';
import {
MetricsexplorertypesMetricMetadataDTO,
MetrictypesTemporalityDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import TimeSeries from '../TimeSeries';
import { TimeSeriesProps } from '../types';
type MockUpdateMetricMetadata = UseMutationResult<
SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse,
Error,
UseUpdateMetricMetadataProps
>;
const mockUpdateMetricMetadata = jest.fn();
jest
.spyOn(useUpdateMetricMetadataHooks, 'useUpdateMetricMetadata')
.mockReturnValue(({
mutate: mockUpdateMetricMetadata,
isLoading: false,
} as Partial<MockUpdateMetricMetadata>) as MockUpdateMetricMetadata);
const updateMetricMetadataSpy = jest.spyOn(
metricsExplorerHooks,
'useUpdateMetricMetadata',
);
type UseUpdateMetricMetadataReturnType = ReturnType<
typeof metricsExplorerHooks.useUpdateMetricMetadata
>;
jest.mock('container/TimeSeriesView/TimeSeriesView', () => ({
__esModule: true,
@@ -60,11 +54,11 @@ jest.mock('react-redux', () => ({
}),
}));
const mockMetric: MetricMetadata = {
type: MetricType.SUM,
const mockMetric: MetricsexplorertypesMetricMetadataDTO = {
type: MetrictypesTypeDTO.sum,
description: 'metric1 description',
unit: 'metric1 unit',
temporality: Temporality.CUMULATIVE,
temporality: MetrictypesTemporalityDTO.cumulative,
isMonotonic: true,
};
@@ -96,6 +90,13 @@ function renderTimeSeries(
}
describe('TimeSeries', () => {
beforeEach(() => {
updateMetricMetadataSpy.mockReturnValue(({
mutate: mockUpdateMetricMetadata,
isLoading: false,
} as Partial<UseUpdateMetricMetadataReturnType>) as UseUpdateMetricMetadataReturnType);
});
it('should render a warning icon when a metric has no unit among multiple metrics', () => {
const user = userEvent.setup();
const { container } = renderTimeSeries({
@@ -133,18 +134,17 @@ describe('TimeSeries', () => {
);
});
// TODO: Unskip this test once the save unit button is implemented
// Tracking at - https://github.com/SigNoz/engineering-pod/issues/3495
it.skip('shows Save unit button when metric had no unit but one is selected', () => {
it('shows Save unit button when metric had no unit but one is selected', async () => {
const { findByText, getByRole } = renderTimeSeries({
metricUnits: [undefined],
metricNames: ['metric1'],
metrics: [mockMetric],
yAxisUnit: 'seconds',
showYAxisUnitSelector: true,
});
expect(
findByText('Save the selected unit for this metric?'),
await findByText('Save the selected unit for this metric?'),
).toBeInTheDocument();
const yesButton = getByRole('button', { name: 'Yes' });
@@ -152,24 +152,25 @@ describe('TimeSeries', () => {
expect(yesButton).toBeEnabled();
});
// TODO: Unskip this test once the save unit button is implemented
// Tracking at - https://github.com/SigNoz/engineering-pod/issues/3495
it.skip('clicking on save unit button shoould upated metric metadata', () => {
it('clicking on save unit button shoould upated metric metadata', async () => {
const user = userEvent.setup();
const { getByRole } = renderTimeSeries({
metricUnits: [''],
metricNames: ['metric1'],
metrics: [mockMetric],
yAxisUnit: 'seconds',
showYAxisUnitSelector: true,
});
const yesButton = getByRole('button', { name: /Yes/i });
user.click(yesButton);
await user.click(yesButton);
expect(mockUpdateMetricMetadata).toHaveBeenCalledWith(
{
metricName: 'metric1',
payload: expect.objectContaining({ unit: 'seconds' }),
pathParams: {
metricName: 'metric1',
},
data: expect.objectContaining({ unit: 'seconds' }),
},
expect.objectContaining({
onSuccess: expect.any(Function),

View File

@@ -1,14 +1,14 @@
import { UseQueryResult } from 'react-query';
import { renderHook } from '@testing-library/react';
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import {
GetMetricMetadata200,
MetricsexplorertypesMetricMetadataDTO,
MetrictypesTemporalityDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { AxiosResponse } from 'axios';
import { initialQueriesMap } from 'constants/queryBuilder';
import * as useGetMultipleMetricsHook from 'hooks/metricsExplorer/useGetMultipleMetrics';
import { SuccessResponseV2 } from 'types/api';
import {
MetricMetadata,
MetricMetadataResponse,
} from 'types/api/metricsExplorer/v2/getMetricMetadata';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderFormula,
@@ -91,11 +91,11 @@ describe('splitQueryIntoOneChartPerQuery', () => {
});
});
const MOCK_METRIC_METADATA: MetricMetadata = {
const MOCK_METRIC_METADATA: MetricsexplorertypesMetricMetadataDTO = {
description: 'Metric 1 description',
unit: 'unit1',
type: MetricType.GAUGE,
temporality: Temporality.DELTA,
type: MetrictypesTypeDTO.gauge,
temporality: MetrictypesTemporalityDTO.delta,
isMonotonic: true,
};
@@ -104,19 +104,16 @@ describe('useGetMetrics', () => {
jest
.spyOn(useGetMultipleMetricsHook, 'useGetMultipleMetrics')
.mockReturnValue([
({
{
isLoading: false,
isError: false,
data: {
httpStatusCode: 200,
data: {
status: 'success',
data: MOCK_METRIC_METADATA,
status: 'success',
},
},
} as Partial<
UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>
>) as UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>,
} as UseQueryResult<AxiosResponse<GetMetricMetadata200>, Error>,
]);
});
@@ -133,12 +130,11 @@ describe('useGetMetrics', () => {
jest
.spyOn(useGetMultipleMetricsHook, 'useGetMultipleMetrics')
.mockReturnValue([
({
{
isLoading: true,
isError: false,
} as Partial<
UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>
>) as UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>,
data: undefined,
} as UseQueryResult<AxiosResponse<GetMetricMetadata200>, Error>,
]);
const { result } = renderHook(() => useGetMetrics(['metric1']));
expect(result.current.metrics).toHaveLength(1);

View File

@@ -1,9 +1,9 @@
import { Dispatch, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query';
import { MetricsexplorertypesMetricMetadataDTO } from 'api/generated/services/sigNoz.schemas';
import { RelatedMetric } from 'api/metricsExplorer/getRelatedMetrics';
import { SuccessResponse, Warning } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
export enum ExplorerTabs {
TIME_SERIES = 'time-series',
@@ -18,7 +18,7 @@ export interface TimeSeriesProps {
isMetricUnitsError: boolean;
metricUnits: (string | undefined)[];
metricNames: string[];
metrics: (MetricMetadata | undefined)[];
metrics: (MetricsexplorertypesMetricMetadataDTO | undefined)[];
handleOpenMetricDetails: (metricName: string) => void;
yAxisUnit: string | undefined;
setYAxisUnit: (unit: string) => void;

View File

@@ -1,9 +1,12 @@
import { UpdateMetricMetadataMutationBody } from 'api/generated/services/metrics';
import { MetricsexplorertypesMetricMetadataDTO } from 'api/generated/services/sigNoz.schemas';
import { mapMetricUnitToUniversalUnit } from 'components/YAxisUnitSelector/utils';
import { useGetMultipleMetrics } from 'hooks/metricsExplorer/useGetMultipleMetrics';
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
import { determineIsMonotonicV2 } from '../MetricDetails/utils';
/**
* Split a query with multiple queryData to multiple distinct queries, each with a single queryData.
* @param query - The query to split
@@ -68,16 +71,14 @@ export function useGetMetrics(
): {
isLoading: boolean;
isError: boolean;
metrics: (MetricMetadata | undefined)[];
metrics: (MetricsexplorertypesMetricMetadataDTO | undefined)[];
} {
const metricsData = useGetMultipleMetrics(metricNames, {
enabled: metricNames.length > 0 && isEnabled,
});
return {
isLoading: metricsData.some((metric) => metric.isLoading),
metrics: metricsData
.map((metric) => metric.data?.data)
.map((data) => data?.data),
metrics: metricsData.map((metric) => metric.data?.data?.data),
isError: metricsData.some((metric) => metric.isError),
};
}
@@ -89,9 +90,24 @@ export function useGetMetrics(
* @returns The units of the metrics, can be undefined if the metric has no unit
*/
export function getMetricUnits(
metrics: (MetricMetadata | undefined)[],
metrics: (MetricsexplorertypesMetricMetadataDTO | undefined)[],
): (string | undefined)[] {
return metrics
.map((metric) => metric?.unit)
.map((unit) => mapMetricUnitToUniversalUnit(unit) || undefined);
}
export function buildUpdateMetricYAxisUnitPayload(
metricName: string,
metric: MetricsexplorertypesMetricMetadataDTO,
yAxisUnit: string | undefined,
): UpdateMetricMetadataMutationBody {
return {
metricName,
type: metric?.type,
description: metric?.description,
unit: yAxisUnit || '',
temporality: metric?.temporality,
isMonotonic: determineIsMonotonicV2(metric?.type, metric?.temporality),
};
}

View File

@@ -1,3 +1,7 @@
import {
MetrictypesTemporalityDTO,
MetrictypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { SpaceAggregation, TimeAggregation } from 'api/v5/v5';
@@ -46,6 +50,29 @@ export function formatNumberToCompactFormat(num: number): string {
}).format(num);
}
export function determineIsMonotonicV2(
metricType: MetrictypesTypeDTO,
temporality?: MetrictypesTemporalityDTO,
): boolean {
if (
metricType === MetrictypesTypeDTO.histogram ||
metricType === MetrictypesTypeDTO.exponentialhistogram
) {
return true;
}
if (
metricType === MetrictypesTypeDTO.gauge ||
metricType === MetrictypesTypeDTO.summary
) {
return false;
}
if (metricType === MetrictypesTypeDTO.sum) {
return temporality === MetrictypesTemporalityDTO.cumulative;
}
return false;
}
// TODO: Remove this after API migration is complete
export function determineIsMonotonic(
metricType: MetricType,
temporality?: Temporality,

View File

@@ -1,32 +1,38 @@
import { useQueries, UseQueryOptions, UseQueryResult } from 'react-query';
import { getMetricMetadata } from 'api/metricsExplorer/v2/getMetricMetadata';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { SuccessResponseV2 } from 'types/api';
import { MetricMetadataResponse } from 'types/api/metricsExplorer/v2/getMetricMetadata';
import {
getGetMetricMetadataQueryKey,
getMetricMetadata,
} from 'api/generated/services/metrics';
import { GetMetricMetadata200 } from 'api/generated/services/sigNoz.schemas';
import { AxiosResponse } from 'axios';
type QueryResult = UseQueryResult<
SuccessResponseV2<MetricMetadataResponse>,
Error
>;
type QueryResult = UseQueryResult<AxiosResponse<GetMetricMetadata200>, Error>;
type UseGetMultipleMetrics = (
metricNames: string[],
options?: UseQueryOptions<SuccessResponseV2<MetricMetadataResponse>, Error>,
options?: UseQueryOptions<AxiosResponse<GetMetricMetadata200>, Error>,
headers?: Record<string, string>,
) => QueryResult[];
export const useGetMultipleMetrics: UseGetMultipleMetrics = (
metricNames,
options,
headers,
) =>
useQueries(
metricNames.map(
(metricName) =>
({
queryKey: [REACT_QUERY_KEY.GET_METRIC_METADATA, metricName],
queryFn: ({ signal }) => getMetricMetadata(metricName, signal, headers),
queryKey: getGetMetricMetadataQueryKey({
metricName,
}),
queryFn: ({ signal }) =>
getMetricMetadata(
{
metricName,
},
signal,
),
...options,
} as UseQueryOptions<SuccessResponseV2<MetricMetadataResponse>, Error>),
} as UseQueryOptions<AxiosResponse<GetMetricMetadata200>, Error>),
),
);

View File

@@ -1,7 +1,7 @@
import { renderHook } from '@testing-library/react';
import { MetricsexplorertypesMetricMetadataDTO } from 'api/generated/services/sigNoz.schemas';
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
import { useGetMetrics } from 'container/MetricsExplorer/Explorer/utils';
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
@@ -24,13 +24,13 @@ const mockUseGetMetrics = useGetMetrics as jest.MockedFunction<
const MOCK_METRIC_1 = {
unit: UniversalYAxisUnit.BYTES,
} as MetricMetadata;
} as MetricsexplorertypesMetricMetadataDTO;
const MOCK_METRIC_2 = {
unit: UniversalYAxisUnit.SECONDS,
} as MetricMetadata;
} as MetricsexplorertypesMetricMetadataDTO;
const MOCK_METRIC_3 = {
unit: '',
} as MetricMetadata;
} as MetricsexplorertypesMetricMetadataDTO;
function createMockCurrentQuery(
queryType: EQueryType,