mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-21 03:10:27 +01:00
Compare commits
1 Commits
fix/chart-
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b229052a39 |
@@ -76,6 +76,9 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
||||
queryRangeMutation.mutate(queryPayload, {
|
||||
onSuccess: (data) => {
|
||||
const updatedQuery = mapQueryDataFromApi(data.data.compositeQuery);
|
||||
if (widget.query.queryType) {
|
||||
updatedQuery.queryType = widget.query.queryType;
|
||||
}
|
||||
// If widget has a y-axis unit, set it to the updated query if it is not already set
|
||||
if (widget.yAxisUnit && !isEmpty(widget.yAxisUnit)) {
|
||||
updatedQuery.unit = widget.yAxisUnit;
|
||||
|
||||
@@ -112,15 +112,14 @@ export const getUPlotChartData = (
|
||||
const processAnomalyDetectionData = (
|
||||
anomalyDetectionData: any,
|
||||
isDarkMode: boolean,
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
): Record<string, { data: (number | null)[][]; color: string }> => {
|
||||
): Record<string, { data: number[][]; color: string }> => {
|
||||
if (!anomalyDetectionData) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const processedData: Record<
|
||||
string,
|
||||
{ data: (number | null)[][]; color: string; legendLabel: string }
|
||||
{ data: number[][]; color: string; legendLabel: string }
|
||||
> = {};
|
||||
|
||||
for (
|
||||
@@ -149,30 +148,24 @@ const processAnomalyDetectionData = (
|
||||
anomalyDetectionData.length > 1 ? `${queryName}-${label}` : label;
|
||||
|
||||
// Single iteration instead of 5 separate map operations
|
||||
const { values: seriesValues } = series?.[index] || { values: [] };
|
||||
const { values: predictedValues } = predictedSeries?.[index] || {
|
||||
values: [],
|
||||
};
|
||||
const { values: upperBoundValues } = upperBoundSeries?.[index] || {
|
||||
values: [],
|
||||
};
|
||||
const { values: lowerBoundValues } = lowerBoundSeries?.[index] || {
|
||||
values: [],
|
||||
};
|
||||
const { values: seriesValues } = series[index];
|
||||
const { values: predictedValues } = predictedSeries[index];
|
||||
const { values: upperBoundValues } = upperBoundSeries[index];
|
||||
const { values: lowerBoundValues } = lowerBoundSeries[index];
|
||||
const length = seriesValues.length;
|
||||
|
||||
const timestamps: number[] = new Array(length);
|
||||
const values: number[] = new Array(length);
|
||||
const predicted: (number | null)[] = new Array(length);
|
||||
const upperBound: (number | null)[] = new Array(length);
|
||||
const lowerBound: (number | null)[] = new Array(length);
|
||||
const predicted: number[] = new Array(length);
|
||||
const upperBound: number[] = new Array(length);
|
||||
const lowerBound: number[] = new Array(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
timestamps[i] = seriesValues[i].timestamp / 1000;
|
||||
values[i] = seriesValues[i].value;
|
||||
predicted[i] = predictedValues[i]?.value ?? null;
|
||||
upperBound[i] = upperBoundValues[i]?.value ?? null;
|
||||
lowerBound[i] = lowerBoundValues[i]?.value ?? null;
|
||||
predicted[i] = predictedValues[i].value;
|
||||
upperBound[i] = upperBoundValues[i].value;
|
||||
lowerBound[i] = lowerBoundValues[i].value;
|
||||
}
|
||||
|
||||
processedData[objKey] = {
|
||||
@@ -192,10 +185,7 @@ const processAnomalyDetectionData = (
|
||||
export const getUplotChartDataForAnomalyDetection = (
|
||||
apiResponse: MetricRangePayloadProps,
|
||||
isDarkMode: boolean,
|
||||
): Record<
|
||||
string,
|
||||
{ [x: string]: any; data: (number | null)[][]; color: string }
|
||||
> => {
|
||||
): Record<string, { [x: string]: any; data: number[][]; color: string }> => {
|
||||
const anomalyDetectionData = apiResponse?.data?.newResult?.data?.result;
|
||||
return processAnomalyDetectionData(anomalyDetectionData, isDarkMode);
|
||||
};
|
||||
|
||||
@@ -1,470 +0,0 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import {
|
||||
getUPlotChartData,
|
||||
getUplotChartDataForAnomalyDetection,
|
||||
} from '../getUplotChartData';
|
||||
|
||||
describe('getUPlotChartData', () => {
|
||||
test('should return empty array with timestamps when no series data', () => {
|
||||
const result = getUPlotChartData(undefined);
|
||||
expect(result).toEqual([[]]);
|
||||
});
|
||||
|
||||
test('should return timestamps and values for single series', () => {
|
||||
const apiResponse = ({
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
metric: {},
|
||||
values: [
|
||||
[1000, '10'],
|
||||
[2000, '20'],
|
||||
[3000, '30'],
|
||||
],
|
||||
queryName: 'A',
|
||||
legend: '',
|
||||
},
|
||||
],
|
||||
resultType: 'matrix',
|
||||
newResult: { data: { result: [] } },
|
||||
},
|
||||
} as unknown) as MetricRangePayloadProps;
|
||||
|
||||
const result = getUPlotChartData(apiResponse);
|
||||
|
||||
expect(result[0]).toEqual([1000, 2000, 3000]); // timestamps
|
||||
expect(result[1]).toEqual([10, 20, 30]); // values
|
||||
});
|
||||
|
||||
test('should handle multiple series with different timestamps', () => {
|
||||
const apiResponse = ({
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
metric: {},
|
||||
values: [
|
||||
[1000, '10'],
|
||||
[2000, '20'],
|
||||
],
|
||||
queryName: 'A',
|
||||
legend: '',
|
||||
},
|
||||
{
|
||||
metric: {},
|
||||
values: [
|
||||
[1000, '100'],
|
||||
[3000, '300'],
|
||||
],
|
||||
queryName: 'B',
|
||||
legend: '',
|
||||
},
|
||||
],
|
||||
resultType: 'matrix',
|
||||
newResult: { data: { result: [] } },
|
||||
},
|
||||
} as unknown) as MetricRangePayloadProps;
|
||||
|
||||
const result = getUPlotChartData(apiResponse);
|
||||
|
||||
// All unique timestamps sorted
|
||||
expect(result[0]).toEqual([1000, 2000, 3000]);
|
||||
// First series: has value at 1000, 2000, missing at 3000
|
||||
expect(result[1]).toEqual([10, 20, null]);
|
||||
// Second series: has value at 1000, missing at 2000, has at 3000
|
||||
expect(result[2]).toEqual([100, null, 300]);
|
||||
});
|
||||
|
||||
test('should handle stacked bar chart', () => {
|
||||
const apiResponse = ({
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
metric: {},
|
||||
values: [
|
||||
[1000, '10'],
|
||||
[2000, '20'],
|
||||
],
|
||||
queryName: 'A',
|
||||
legend: '',
|
||||
},
|
||||
{
|
||||
metric: {},
|
||||
values: [
|
||||
[1000, '5'],
|
||||
[2000, '10'],
|
||||
],
|
||||
queryName: 'B',
|
||||
legend: '',
|
||||
},
|
||||
],
|
||||
resultType: 'matrix',
|
||||
newResult: { data: { result: [] } },
|
||||
},
|
||||
} as unknown) as MetricRangePayloadProps;
|
||||
|
||||
const result = getUPlotChartData(apiResponse, false, true);
|
||||
|
||||
expect(result[0]).toEqual([1000, 2000]); // timestamps
|
||||
// Stacked: first series = its value + second series value
|
||||
expect(result[1]).toEqual([15, 30]); // 10+5, 20+10
|
||||
expect(result[2]).toEqual([5, 10]); // second series unchanged
|
||||
});
|
||||
|
||||
test('should handle invalid values like +Inf', () => {
|
||||
const apiResponse = ({
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
metric: {},
|
||||
values: [
|
||||
[1000, '10'],
|
||||
[2000, '+Inf'],
|
||||
[3000, 'NaN'],
|
||||
],
|
||||
queryName: 'A',
|
||||
legend: '',
|
||||
},
|
||||
],
|
||||
resultType: 'matrix',
|
||||
newResult: { data: { result: [] } },
|
||||
},
|
||||
} as unknown) as MetricRangePayloadProps;
|
||||
|
||||
const result = getUPlotChartData(apiResponse);
|
||||
|
||||
expect(result[0]).toEqual([1000, 2000, 3000]);
|
||||
expect(result[1]).toEqual([10, null, null]); // Invalid values become null
|
||||
});
|
||||
|
||||
test('should handle series with empty values array', () => {
|
||||
const apiResponse = ({
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
metric: {},
|
||||
values: [],
|
||||
queryName: 'A',
|
||||
legend: '',
|
||||
},
|
||||
],
|
||||
resultType: 'matrix',
|
||||
newResult: { data: { result: [] } },
|
||||
},
|
||||
} as unknown) as MetricRangePayloadProps;
|
||||
|
||||
const result = getUPlotChartData(apiResponse);
|
||||
|
||||
expect(result[0]).toEqual([]); // No timestamps
|
||||
expect(result[1]).toEqual([]); // Empty values
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUplotChartDataForAnomalyDetection', () => {
|
||||
const createSeriesItem = (
|
||||
labels: Record<string, string>,
|
||||
values: Array<{ timestamp: number; value: string }>,
|
||||
) => ({
|
||||
labels,
|
||||
labelsArray: Object.entries(labels).map(([k, v]) => ({ [k]: v })),
|
||||
values,
|
||||
});
|
||||
|
||||
const createAnomalyResponse = (
|
||||
overrides: Partial<{
|
||||
series: ReturnType<typeof createSeriesItem>[];
|
||||
predictedSeries: ReturnType<typeof createSeriesItem>[];
|
||||
upperBoundSeries: ReturnType<typeof createSeriesItem>[];
|
||||
lowerBoundSeries: ReturnType<typeof createSeriesItem>[];
|
||||
queryName: string;
|
||||
legend: string;
|
||||
}> = {},
|
||||
): MetricRangePayloadProps =>
|
||||
(({
|
||||
data: {
|
||||
newResult: {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
series: overrides.series ?? [
|
||||
createSeriesItem({ service: 'test-service' }, [
|
||||
{ timestamp: 1000000, value: '10' },
|
||||
{ timestamp: 2000000, value: '20' },
|
||||
]),
|
||||
],
|
||||
predictedSeries: overrides.predictedSeries ?? [
|
||||
createSeriesItem({ service: 'test-service' }, [
|
||||
{ timestamp: 1000000, value: '12' },
|
||||
{ timestamp: 2000000, value: '22' },
|
||||
]),
|
||||
],
|
||||
upperBoundSeries: overrides.upperBoundSeries ?? [
|
||||
createSeriesItem({ service: 'test-service' }, [
|
||||
{ timestamp: 1000000, value: '15' },
|
||||
{ timestamp: 2000000, value: '25' },
|
||||
]),
|
||||
],
|
||||
lowerBoundSeries: overrides.lowerBoundSeries ?? [
|
||||
createSeriesItem({ service: 'test-service' }, [
|
||||
{ timestamp: 1000000, value: '8' },
|
||||
{ timestamp: 2000000, value: '18' },
|
||||
]),
|
||||
],
|
||||
queryName: overrides.queryName ?? 'A',
|
||||
legend: overrides.legend ?? '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
resultType: 'matrix',
|
||||
},
|
||||
} as unknown) as MetricRangePayloadProps);
|
||||
|
||||
test('should return empty object when anomalyDetectionData is undefined', () => {
|
||||
const result = getUplotChartDataForAnomalyDetection(
|
||||
{ data: { resultType: 'matrix' } } as MetricRangePayloadProps,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
test('should process anomaly detection data correctly', () => {
|
||||
const apiResponse = createAnomalyResponse({});
|
||||
const result = getUplotChartDataForAnomalyDetection(apiResponse, true);
|
||||
|
||||
expect(Object.keys(result)).toHaveLength(1);
|
||||
const seriesKey = Object.keys(result)[0];
|
||||
const seriesData = result[seriesKey];
|
||||
|
||||
// Data should have 5 arrays: timestamps, values, predicted, upperBound, lowerBound
|
||||
expect(seriesData.data).toHaveLength(5);
|
||||
|
||||
// Timestamps (converted from ms to seconds)
|
||||
expect(seriesData.data[0]).toEqual([1000, 2000]);
|
||||
// Values (stored as strings from API)
|
||||
expect(seriesData.data[1]).toEqual(['10', '20']);
|
||||
// Predicted
|
||||
expect(seriesData.data[2]).toEqual(['12', '22']);
|
||||
// Upper bound
|
||||
expect(seriesData.data[3]).toEqual(['15', '25']);
|
||||
// Lower bound
|
||||
expect(seriesData.data[4]).toEqual(['8', '18']);
|
||||
|
||||
expect(seriesData.color).toBeDefined();
|
||||
expect(seriesData.legendLabel).toBeDefined();
|
||||
});
|
||||
|
||||
test('should handle multiple queries with unique keys', () => {
|
||||
const apiResponse = ({
|
||||
data: {
|
||||
newResult: {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
series: [
|
||||
createSeriesItem({ service: 'service-a' }, [
|
||||
{ timestamp: 1000000, value: '10' },
|
||||
]),
|
||||
],
|
||||
predictedSeries: [
|
||||
createSeriesItem({ service: 'service-a' }, [
|
||||
{ timestamp: 1000000, value: '12' },
|
||||
]),
|
||||
],
|
||||
upperBoundSeries: [
|
||||
createSeriesItem({ service: 'service-a' }, [
|
||||
{ timestamp: 1000000, value: '15' },
|
||||
]),
|
||||
],
|
||||
lowerBoundSeries: [
|
||||
createSeriesItem({ service: 'service-a' }, [
|
||||
{ timestamp: 1000000, value: '8' },
|
||||
]),
|
||||
],
|
||||
queryName: 'QueryA',
|
||||
legend: '',
|
||||
},
|
||||
{
|
||||
series: [
|
||||
createSeriesItem({ service: 'service-b' }, [
|
||||
{ timestamp: 1000000, value: '100' },
|
||||
]),
|
||||
],
|
||||
predictedSeries: [
|
||||
createSeriesItem({ service: 'service-b' }, [
|
||||
{ timestamp: 1000000, value: '120' },
|
||||
]),
|
||||
],
|
||||
upperBoundSeries: [
|
||||
createSeriesItem({ service: 'service-b' }, [
|
||||
{ timestamp: 1000000, value: '150' },
|
||||
]),
|
||||
],
|
||||
lowerBoundSeries: [
|
||||
createSeriesItem({ service: 'service-b' }, [
|
||||
{ timestamp: 1000000, value: '80' },
|
||||
]),
|
||||
],
|
||||
queryName: 'QueryB',
|
||||
legend: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
resultType: 'matrix',
|
||||
},
|
||||
} as unknown) as MetricRangePayloadProps;
|
||||
|
||||
const result = getUplotChartDataForAnomalyDetection(apiResponse, true);
|
||||
|
||||
// Should have 2 series with queryName prefix since multiple queries
|
||||
expect(Object.keys(result)).toHaveLength(2);
|
||||
expect(Object.keys(result).some((key) => key.startsWith('QueryA-'))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(Object.keys(result).some((key) => key.startsWith('QueryB-'))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// Issue #11022: Anomaly alert preview bug with empty prediction series
|
||||
describe('Issue #11022 - Empty prediction series handling', () => {
|
||||
test('should not crash when predictedSeries is empty array', () => {
|
||||
const apiResponse = createAnomalyResponse({
|
||||
predictedSeries: [],
|
||||
});
|
||||
|
||||
// This should not throw
|
||||
expect(() =>
|
||||
getUplotChartDataForAnomalyDetection(apiResponse, true),
|
||||
).not.toThrow();
|
||||
|
||||
const result = getUplotChartDataForAnomalyDetection(apiResponse, true);
|
||||
const seriesKey = Object.keys(result)[0];
|
||||
|
||||
// Should still have valid structure with null predicted values
|
||||
expect(result[seriesKey].data).toHaveLength(5);
|
||||
// Predicted values should be null when predictedSeries is empty
|
||||
expect(result[seriesKey].data[2]).toEqual([null, null]);
|
||||
});
|
||||
|
||||
test('should not crash when upperBoundSeries is empty array', () => {
|
||||
const apiResponse = createAnomalyResponse({
|
||||
upperBoundSeries: [],
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
getUplotChartDataForAnomalyDetection(apiResponse, true),
|
||||
).not.toThrow();
|
||||
|
||||
const result = getUplotChartDataForAnomalyDetection(apiResponse, true);
|
||||
const seriesKey = Object.keys(result)[0];
|
||||
|
||||
// Upper bound values should be null when upperBoundSeries is empty
|
||||
expect(result[seriesKey].data[3]).toEqual([null, null]);
|
||||
});
|
||||
|
||||
test('should not crash when lowerBoundSeries is empty array', () => {
|
||||
const apiResponse = createAnomalyResponse({
|
||||
lowerBoundSeries: [],
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
getUplotChartDataForAnomalyDetection(apiResponse, true),
|
||||
).not.toThrow();
|
||||
|
||||
const result = getUplotChartDataForAnomalyDetection(apiResponse, true);
|
||||
const seriesKey = Object.keys(result)[0];
|
||||
|
||||
// Lower bound values should be null when lowerBoundSeries is empty
|
||||
expect(result[seriesKey].data[4]).toEqual([null, null]);
|
||||
});
|
||||
|
||||
test('should not crash when all bound series are empty arrays', () => {
|
||||
const apiResponse = createAnomalyResponse({
|
||||
predictedSeries: [],
|
||||
upperBoundSeries: [],
|
||||
lowerBoundSeries: [],
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
getUplotChartDataForAnomalyDetection(apiResponse, true),
|
||||
).not.toThrow();
|
||||
|
||||
const result = getUplotChartDataForAnomalyDetection(apiResponse, true);
|
||||
const seriesKey = Object.keys(result)[0];
|
||||
|
||||
// Should have structure with values from series but null predictions/bounds
|
||||
expect(result[seriesKey].data).toHaveLength(5);
|
||||
expect(result[seriesKey].data[0]).toEqual([1000, 2000]); // timestamps from series
|
||||
expect(result[seriesKey].data[1]).toEqual(['10', '20']); // values from series (strings from API)
|
||||
expect(result[seriesKey].data[2]).toEqual([null, null]); // predicted null
|
||||
expect(result[seriesKey].data[3]).toEqual([null, null]); // upper bound null
|
||||
expect(result[seriesKey].data[4]).toEqual([null, null]); // lower bound null
|
||||
});
|
||||
|
||||
test('should not crash when series exists but prediction arrays have fewer elements', () => {
|
||||
const apiResponse = ({
|
||||
data: {
|
||||
newResult: {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
series: [
|
||||
createSeriesItem({ service: 'test' }, [
|
||||
{ timestamp: 1000000, value: '10' },
|
||||
{ timestamp: 2000000, value: '20' },
|
||||
]),
|
||||
createSeriesItem({ service: 'test2' }, [
|
||||
{ timestamp: 1000000, value: '30' },
|
||||
]),
|
||||
],
|
||||
// Only one element in prediction arrays, but series has two
|
||||
predictedSeries: [
|
||||
createSeriesItem({ service: 'test' }, [
|
||||
{ timestamp: 1000000, value: '12' },
|
||||
]),
|
||||
],
|
||||
upperBoundSeries: [
|
||||
createSeriesItem({ service: 'test' }, [
|
||||
{ timestamp: 1000000, value: '15' },
|
||||
]),
|
||||
],
|
||||
lowerBoundSeries: [
|
||||
createSeriesItem({ service: 'test' }, [
|
||||
{ timestamp: 1000000, value: '8' },
|
||||
]),
|
||||
],
|
||||
queryName: 'A',
|
||||
legend: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
resultType: 'matrix',
|
||||
},
|
||||
} as unknown) as MetricRangePayloadProps;
|
||||
|
||||
// This tests the case where series[1] exists but predictedSeries[1] is undefined
|
||||
expect(() =>
|
||||
getUplotChartDataForAnomalyDetection(apiResponse, true),
|
||||
).not.toThrow();
|
||||
|
||||
const result = getUplotChartDataForAnomalyDetection(apiResponse, true);
|
||||
|
||||
// Should have 2 series processed
|
||||
expect(Object.keys(result)).toHaveLength(2);
|
||||
|
||||
// The second series should have null values for predictions/bounds
|
||||
const secondKey = Object.keys(result).find((k) =>
|
||||
k.includes('test2'),
|
||||
) as string;
|
||||
expect(result[secondKey].data[2]).toEqual([null]); // predicted
|
||||
expect(result[secondKey].data[3]).toEqual([null]); // upper bound
|
||||
expect(result[secondKey].data[4]).toEqual([null]); // lower bound
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user