Compare commits

...

12 Commits

Author SHA1 Message Date
Abhi kumar
e99000ed88 Merge branch 'main' into fix/issue-3917 2026-02-24 16:24:16 +05:30
Vinicius Lourenço
cb1a2a8a13 perf(bundle-size): lazy load pages to reduce main bundle size (#10230)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
2026-02-24 10:41:40 +00:00
Nikhil Soni
1a5d37b25a fix: add missing filtering for ip address for scalar data (#10264)
* fix: add missing filtering for ip address for scalar data

In domain listing api for external api monitoring,
we have option to filter out the IP address but
it only handles timeseries and raw type data while
domain list handler returns scalar data.

* fix: switch to new derived attributes for ip filtering

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2026-02-24 10:26:10 +00:00
Piyush Singariya
bc4273f2f8 chore: test clickhouse version 25.12.5 (#10402) 2026-02-24 14:55:51 +05:30
Abhi Kumar
9e3b02a021 Merge branch 'main' of https://github.com/SigNoz/signoz into fix/issue-3917 2026-02-24 14:32:21 +05:30
Abhi Kumar
a52fc9cf96 chore: fixed unit tests 2026-02-24 14:12:05 +05:30
Abhi Kumar
2d96ca300c Merge branch 'main' of https://github.com/SigNoz/signoz into fix/issue-3917 2026-02-24 13:48:47 +05:30
Abhi kumar
c0684dbf79 Merge branch 'fix/issue-6354' into fix/issue-3917 2026-02-23 16:58:50 +05:30
Abhi Kumar
5133346f77 Merge branch 'main' of https://github.com/SigNoz/signoz into fix/issue-6354 2026-02-23 16:58:31 +05:30
Abhi Kumar
d48495aecc fix: fixed tsc 2026-02-23 16:58:14 +05:30
Abhi Kumar
67184b4064 fix: thresholds working correctly with number panel 2026-02-23 16:46:36 +05:30
Abhi Kumar
53d7753167 fix: fixed unit converstion support across thresholds and yaxisunit 2026-02-23 16:32:25 +05:30
11 changed files with 176 additions and 82 deletions

View File

@@ -54,7 +54,7 @@ jobs:
- sqlite
clickhouse-version:
- 25.5.6
- 25.10.5
- 25.12.5
schema-migrator-version:
- v0.142.0
postgres-version:

View File

@@ -308,3 +308,15 @@ export const PublicDashboardPage = Loadable(
/* webpackChunkName: "Public Dashboard Page" */ 'pages/PublicDashboard'
),
);
export const AlertTypeSelectionPage = Loadable(
() =>
import(
/* webpackChunkName: "Alert Type Selection Page" */ 'pages/AlertTypeSelection'
),
);
export const MeterExplorerPage = Loadable(
() =>
import(/* webpackChunkName: "Meter Explorer Page" */ 'pages/MeterExplorer'),
);

View File

@@ -1,12 +1,10 @@
import { RouteProps } from 'react-router-dom';
import ROUTES from 'constants/routes';
import AlertTypeSelectionPage from 'pages/AlertTypeSelection';
import MessagingQueues from 'pages/MessagingQueues';
import MeterExplorer from 'pages/MeterExplorer';
import {
AlertHistory,
AlertOverview,
AlertTypeSelectionPage,
AllAlertChannels,
AllErrors,
ApiMonitoring,
@@ -29,6 +27,8 @@ import {
LogsExplorer,
LogsIndexToFields,
LogsSaveViews,
MessagingQueuesMainPage,
MeterExplorerPage,
MetricsExplorer,
OldLogsExplorer,
Onboarding,
@@ -399,28 +399,28 @@ const routes: AppRoutes[] = [
{
path: ROUTES.MESSAGING_QUEUES_KAFKA,
exact: true,
component: MessagingQueues,
component: MessagingQueuesMainPage,
key: 'MESSAGING_QUEUES_KAFKA',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_CELERY_TASK,
exact: true,
component: MessagingQueues,
component: MessagingQueuesMainPage,
key: 'MESSAGING_QUEUES_CELERY_TASK',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_OVERVIEW,
exact: true,
component: MessagingQueues,
component: MessagingQueuesMainPage,
key: 'MESSAGING_QUEUES_OVERVIEW',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_KAFKA_DETAIL,
exact: true,
component: MessagingQueues,
component: MessagingQueuesMainPage,
key: 'MESSAGING_QUEUES_KAFKA_DETAIL',
isPrivate: true,
},
@@ -463,21 +463,21 @@ const routes: AppRoutes[] = [
{
path: ROUTES.METER,
exact: true,
component: MeterExplorer,
component: MeterExplorerPage,
key: 'METER',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER,
exact: true,
component: MeterExplorer,
component: MeterExplorerPage,
key: 'METER_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER_VIEWS,
exact: true,
component: MeterExplorer,
component: MeterExplorerPage,
key: 'METER_EXPLORER_VIEWS',
isPrivate: true,
},

View File

@@ -40,6 +40,7 @@ function ValueGraph({
value,
rawValue,
thresholds,
yAxisUnit,
}: ValueGraphProps): JSX.Element {
const { t } = useTranslation(['valueGraph']);
const containerRef = useRef<HTMLDivElement>(null);
@@ -87,7 +88,7 @@ function ValueGraph({
const {
threshold,
isConflictingThresholds,
} = getBackgroundColorAndThresholdCheck(thresholds, rawValue);
} = getBackgroundColorAndThresholdCheck(thresholds, rawValue, yAxisUnit);
return (
<div
@@ -155,6 +156,7 @@ interface ValueGraphProps {
value: string;
rawValue: number;
thresholds: ThresholdProps[];
yAxisUnit?: string;
}
export default ValueGraph;

View File

@@ -1,9 +1,10 @@
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { evaluateThresholdWithConvertedValue } from 'container/GridTableComponent/utils';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
function compareThreshold(
function doesValueSatisfyThreshold(
rawValue: number,
threshold: ThresholdProps,
yAxisUnit?: string,
): boolean {
if (
threshold.thresholdOperator === undefined ||
@@ -11,31 +12,14 @@ function compareThreshold(
) {
return false;
}
switch (threshold.thresholdOperator) {
case '>':
return rawValue > threshold.thresholdValue;
case '>=':
return rawValue >= threshold.thresholdValue;
case '<':
return rawValue < threshold.thresholdValue;
case '<=':
return rawValue <= threshold.thresholdValue;
case '=':
return rawValue === threshold.thresholdValue;
default:
return false;
}
}
function extractNumbersFromString(inputString: string): number[] {
const regex = /[+-]?\d+(\.\d+)?/g;
const matches = inputString.match(regex);
if (matches) {
return matches.map(Number);
}
return [];
return evaluateThresholdWithConvertedValue(
rawValue,
threshold.thresholdValue,
threshold.thresholdOperator,
threshold.thresholdUnit,
yAxisUnit,
);
}
function getHighestPrecedenceThreshold(
@@ -63,17 +47,13 @@ function getHighestPrecedenceThreshold(
export function getBackgroundColorAndThresholdCheck(
thresholds: ThresholdProps[],
rawValue: number,
yAxisUnit?: string,
): {
threshold: ThresholdProps;
isConflictingThresholds: boolean;
} {
const matchingThresholds = thresholds.filter((threshold) =>
compareThreshold(
extractNumbersFromString(
getYAxisFormattedValue(rawValue.toString(), threshold.thresholdUnit || ''),
)[0],
threshold,
),
doesValueSatisfyThreshold(rawValue, threshold, yAxisUnit),
);
if (matchingThresholds.length === 0) {

View File

@@ -49,7 +49,7 @@ function evaluateCondition(
* @param columnUnit - The current unit of the value.
* @returns A boolean indicating whether the value meets the threshold condition.
*/
function evaluateThresholdWithConvertedValue(
export function evaluateThresholdWithConvertedValue(
value: number,
thresholdValue: number,
thresholdOperator?: string,

View File

@@ -99,6 +99,7 @@ function GridValueComponent({
<ValueGraph
thresholds={thresholds || []}
rawValue={value}
yAxisUnit={yAxisUnit}
value={
yAxisUnit
? getYAxisFormattedValue(

View File

@@ -356,7 +356,10 @@ function Threshold({
)}
</div>
{isInvalidUnitComparison && (
<Typography.Text className="invalid-unit">
<Typography.Text
className="invalid-unit"
data-testid="invalid-unit-comparison"
>
Threshold unit ({unit}) is not valid in comparison with the{' '}
{selectedGraph === PANEL_TYPES.TABLE ? 'column' : 'y-axis'} unit (
{selectedGraph === PANEL_TYPES.TABLE

View File

@@ -1,6 +1,8 @@
/* eslint-disable react/jsx-props-no-spreading */
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { Y_AXIS_UNIT_NAMES } from 'components/YAxisUnitSelector/constants';
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { render, screen } from 'tests/test-utils';
@@ -17,9 +19,18 @@ jest.mock('lib/query/createTableColumnsFromQuery', () => ({
// Mock the unitOptions function
jest.mock('container/NewWidget/utils', () => ({
unitOptions: jest.fn(() => [
{ value: 'none', label: 'None' },
{ value: '%', label: 'Percent (0 - 100)' },
{ value: 'ms', label: 'Milliseconds (ms)' },
{
value: UniversalYAxisUnit.NONE,
label: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.NONE],
},
{
value: UniversalYAxisUnit.PERCENT,
label: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.PERCENT],
},
{
value: UniversalYAxisUnit.MILLISECONDS,
label: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MILLISECONDS],
},
]),
}));
@@ -28,7 +39,7 @@ const defaultProps = {
keyIndex: 0,
thresholdOperator: '>' as const,
thresholdValue: 50,
thresholdUnit: 'none',
thresholdUnit: UniversalYAxisUnit.NONE,
thresholdColor: 'Red',
thresholdFormat: 'Text' as const,
isEditEnabled: true,
@@ -38,8 +49,11 @@ const defaultProps = {
{ value: 'memory_usage', label: 'Memory Usage' },
],
thresholdTableOptions: 'cpu_usage',
columnUnits: { cpu_usage: 'percent', memory_usage: 'bytes' },
yAxisUnit: '%',
columnUnits: {
cpu_usage: UniversalYAxisUnit.PERCENT,
memory_usage: UniversalYAxisUnit.BYTES,
},
yAxisUnit: UniversalYAxisUnit.PERCENT,
moveThreshold: jest.fn(),
};
@@ -68,28 +82,27 @@ describe('Threshold Component Unit Validation', () => {
it('should show validation error when threshold unit is not "none" and units are incompatible', () => {
// Act - Render component with incompatible units (ms vs percent)
renderThreshold({
thresholdUnit: 'ms',
thresholdUnit: UniversalYAxisUnit.MILLISECONDS,
thresholdValue: 50,
});
const errorMessage = screen.getByTestId('invalid-unit-comparison');
// Assert - Validation error should be displayed
expect(
screen.getByText(
/Threshold unit \(ms\) is not valid in comparison with the column unit \(percent\)/i,
),
).toBeInTheDocument();
expect(errorMessage.textContent).toBe(
`Threshold unit (${UniversalYAxisUnit.MILLISECONDS}) is not valid in comparison with the column unit (${UniversalYAxisUnit.PERCENT})`,
);
});
it('should not show validation error when threshold unit matches column unit', () => {
// Act - Render component with matching units
renderThreshold({
thresholdUnit: 'percent',
thresholdUnit: UniversalYAxisUnit.PERCENT,
thresholdValue: 50,
});
// Assert - No validation error should be displayed
expect(
screen.queryByText(/Threshold unit.*is not valid in comparison/i),
screen.queryByTestId('invalid-unit-comparison'),
).not.toBeInTheDocument();
});
@@ -97,17 +110,16 @@ describe('Threshold Component Unit Validation', () => {
// Act - Render component for time series with incompatible units
renderThreshold({
selectedGraph: PANEL_TYPES.TIME_SERIES,
thresholdUnit: 'ms',
thresholdUnit: UniversalYAxisUnit.MILLISECONDS,
thresholdValue: 100,
yAxisUnit: 'percent',
yAxisUnit: UniversalYAxisUnit.PERCENT,
});
const errorMessage = screen.getByTestId('invalid-unit-comparison');
// Assert - Validation error should be displayed
expect(
screen.getByText(
/Threshold unit \(ms\) is not valid in comparison with the y-axis unit \(percent\)/i,
),
).toBeInTheDocument();
expect(errorMessage.textContent).toBe(
`Threshold unit (${UniversalYAxisUnit.MILLISECONDS}) is not valid in comparison with the y-axis unit (${UniversalYAxisUnit.PERCENT})`,
);
});
it('should not show validation error for time series graph when threshold unit is "none"', () => {
@@ -116,43 +128,39 @@ describe('Threshold Component Unit Validation', () => {
selectedGraph: PANEL_TYPES.TIME_SERIES,
thresholdUnit: 'none',
thresholdValue: 100,
yAxisUnit: 'percent',
yAxisUnit: UniversalYAxisUnit.PERCENT,
});
// Assert - No validation error should be displayed
expect(
screen.queryByText(/Threshold unit.*is not valid in comparison/i),
screen.queryByTestId('invalid-unit-comparison'),
).not.toBeInTheDocument();
});
it('should not show validation error when threshold unit is compatible with column unit', () => {
// Act - Render component with compatible units (both in same category - Time)
renderThreshold({
thresholdUnit: 's',
thresholdUnit: UniversalYAxisUnit.SECONDS,
thresholdValue: 100,
columnUnits: { cpu_usage: 'ms' },
columnUnits: { cpu_usage: UniversalYAxisUnit.MILLISECONDS },
thresholdTableOptions: 'cpu_usage',
});
// Assert - No validation error should be displayed
expect(
screen.queryByText(/Threshold unit.*is not valid in comparison/i),
screen.queryByTestId('invalid-unit-comparison'),
).not.toBeInTheDocument();
});
it('should show validation error when threshold unit is in different category than column unit', () => {
// Act - Render component with units from different categories
renderThreshold({
thresholdUnit: 'bytes',
thresholdUnit: UniversalYAxisUnit.BYTES,
thresholdValue: 100,
yAxisUnit: 'percent',
yAxisUnit: UniversalYAxisUnit.PERCENT,
});
const errorMessage = screen.getByTestId('invalid-unit-comparison');
// Assert - Validation error should be displayed
expect(
screen.getByText(
/Threshold unit \(bytes\) is not valid in comparison with the column unit \(percent\)/i,
),
).toBeInTheDocument();
expect(errorMessage.textContent).toBe(
`Threshold unit (${UniversalYAxisUnit.BYTES}) is not valid in comparison with the column unit (${UniversalYAxisUnit.PERCENT})`,
);
});
});

View File

@@ -120,6 +120,8 @@ func FilterResponse(results []*qbtypes.QueryRangeResponse) []*qbtypes.QueryRange
}
}
resultData.Rows = filteredRows
case *qbtypes.ScalarData:
resultData.Data = filterScalarDataIPs(resultData.Columns, resultData.Data)
}
filteredData = append(filteredData, result)
@@ -145,6 +147,39 @@ func shouldIncludeSeries(series *qbtypes.TimeSeries) bool {
return true
}
func filterScalarDataIPs(columns []*qbtypes.ColumnDescriptor, data [][]any) [][]any {
// Find column indices for server address fields
serverColIndices := make([]int, 0)
for i, col := range columns {
if col.Name == derivedKeyHTTPHost {
serverColIndices = append(serverColIndices, i)
}
}
if len(serverColIndices) == 0 {
return data
}
filtered := make([][]any, 0, len(data))
for _, row := range data {
includeRow := true
for _, colIdx := range serverColIndices {
if colIdx < len(row) {
if strVal, ok := row[colIdx].(string); ok {
if net.ParseIP(strVal) != nil {
includeRow = false
break
}
}
}
}
if includeRow {
filtered = append(filtered, row)
}
}
return filtered
}
func shouldIncludeRow(row *qbtypes.RawRow) bool {
if row.Data != nil {
if domainVal, ok := row.Data[derivedKeyHTTPHost]; ok {

View File

@@ -117,6 +117,59 @@ func TestFilterResponse(t *testing.T) {
},
},
},
{
name: "should filter out IP addresses from scalar data",
input: []*qbtypes.QueryRangeResponse{
{
Data: qbtypes.QueryData{
Results: []any{
&qbtypes.ScalarData{
QueryName: "endpoints",
Columns: []*qbtypes.ColumnDescriptor{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: derivedKeyHTTPHost},
Type: qbtypes.ColumnTypeGroup,
},
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "endpoints"},
Type: qbtypes.ColumnTypeAggregation,
},
},
Data: [][]any{
{"192.168.1.1", 10},
{"example.com", 20},
{"10.0.0.1", 5},
},
},
},
},
},
},
expected: []*qbtypes.QueryRangeResponse{
{
Data: qbtypes.QueryData{
Results: []any{
&qbtypes.ScalarData{
QueryName: "endpoints",
Columns: []*qbtypes.ColumnDescriptor{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: derivedKeyHTTPHost},
Type: qbtypes.ColumnTypeGroup,
},
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "endpoints"},
Type: qbtypes.ColumnTypeAggregation,
},
},
Data: [][]any{
{"example.com", 20},
},
},
},
},
},
},
},
}
for _, tt := range tests {