Compare commits

..

1 Commits

Author SHA1 Message Date
primus-bot[bot]
d7a743cea9 chore(release): bump to v0.114.0 (#10496)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-03-05 12:43:16 +05:30
10 changed files with 41 additions and 433 deletions

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.113.0
image: signoz/signoz:v0.114.0
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port
@@ -213,7 +213,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.1
image: signoz/signoz-otel-collector:v0.144.2
entrypoint:
- /bin/sh
command:
@@ -241,7 +241,7 @@ services:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.1
image: signoz/signoz-otel-collector:v0.144.2
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.113.0
image: signoz/signoz:v0.114.0
ports:
- "8080:8080" # signoz port
volumes:
@@ -139,7 +139,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.1
image: signoz/signoz-otel-collector:v0.144.2
entrypoint:
- /bin/sh
command:
@@ -167,7 +167,7 @@ services:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.1
image: signoz/signoz-otel-collector:v0.144.2
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.113.0}
image: signoz/signoz:${VERSION:-v0.114.0}
container_name: signoz
ports:
- "8080:8080" # signoz port
@@ -204,7 +204,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.2}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
@@ -229,7 +229,7 @@ services:
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.2}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.113.0}
image: signoz/signoz:${VERSION:-v0.114.0}
container_name: signoz
ports:
- "8080:8080" # signoz port
@@ -132,7 +132,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.2}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
@@ -157,7 +157,7 @@ services:
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.2}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

View File

@@ -175,10 +175,7 @@ export function PlannedDowntimeForm(
} else {
notifications.error({
message: 'Error',
description:
typeof response.error === 'string'
? response.error
: response.error?.message || 'unexpected_error',
description: response.error || 'unexpected_error',
});
}
} catch (e) {

View File

@@ -359,10 +359,7 @@ export function PlannedDowntimeList({
useEffect(() => {
if (downtimeSchedules.isError) {
notifications.error({
message:
downtimeSchedules.error?.message?.toString() || 'Something went wrong',
});
notifications.error(downtimeSchedules.error);
}
}, [downtimeSchedules.error, downtimeSchedules.isError, notifications]);

View File

@@ -137,10 +137,7 @@ export const deleteDowntimeHandler = ({
export const createEditDowntimeSchedule = async (
props: DowntimeScheduleUpdatePayload,
): Promise<
| SuccessResponse<PayloadProps>
| ErrorResponse<{ code: string; message: string } | string>
> => {
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
if (props.id) {
return updateDowntimeSchedule({ ...props });
}

View File

@@ -1,334 +0,0 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { ReactElement, ReactNode } from 'react';
import { createElement } from 'react';
import { act, renderHook } from '@testing-library/react';
import { NotificationProvider, useNotifications } from '../useNotifications';
const mockSuccess = jest.fn();
const mockError = jest.fn();
const mockInfo = jest.fn();
const mockWarning = jest.fn();
const mockOpen = jest.fn();
const mockDestroy = jest.fn();
jest.mock('antd', () => ({
notification: {
useNotification: () => [
{
success: mockSuccess,
error: mockError,
info: mockInfo,
warning: mockWarning,
open: mockOpen,
destroy: mockDestroy,
},
createElement('div', { 'data-testid': 'notification-holder' }),
],
},
}));
const mockSetExtra = jest.fn();
const mockCaptureMessage = jest.fn();
jest.mock('@sentry/react', () => ({
setExtra: (key: string, value: unknown) => mockSetExtra(key, value),
captureMessage: (msg: string) => mockCaptureMessage(msg),
}));
describe('useNotifications', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('without provider', () => {
it('returns default notification API and does not throw', () => {
const { result } = renderHook(() => useNotifications());
expect(result.current.notifications).toBeDefined();
expect(typeof result.current.notifications.success).toBe('function');
expect(typeof result.current.notifications.error).toBe('function');
expect(typeof result.current.notifications.info).toBe('function');
expect(typeof result.current.notifications.warning).toBe('function');
expect(typeof result.current.notifications.open).toBe('function');
expect(typeof result.current.notifications.destroy).toBe('function');
});
it('no-op methods do not throw when called', () => {
const { result } = renderHook(() => useNotifications());
expect(() => {
result.current.notifications.success({ message: 'Ok' });
result.current.notifications.error({ message: 'Fail' });
result.current.notifications.info({ message: 'Info' });
result.current.notifications.warning({ message: 'Warn' });
result.current.notifications.open({ message: 'Open' });
result.current.notifications.destroy();
result.current.notifications.destroy('key');
}).not.toThrow();
});
});
describe('with NotificationProvider', () => {
const wrapper = ({ children }: { children: ReactNode }) =>
createElement(NotificationProvider, null, children as ReactElement);
it('returns notification API that forwards valid payloads', () => {
const { result } = renderHook(() => useNotifications(), { wrapper });
act(() => {
result.current.notifications.success({
message: 'Saved',
description: 'Your changes were saved.',
});
});
expect(mockSuccess).toHaveBeenCalledTimes(1);
expect(mockSuccess).toHaveBeenCalledWith({
message: 'Saved',
description: 'Your changes were saved.',
});
});
it('forwards valid payloads for error, info, warning, open', () => {
const { result } = renderHook(() => useNotifications(), { wrapper });
act(() => {
result.current.notifications.error({ message: 'Error msg' });
});
expect(mockError).toHaveBeenCalledWith({ message: 'Error msg' });
act(() => {
result.current.notifications.info({ message: 'Info msg' });
});
expect(mockInfo).toHaveBeenCalledWith({ message: 'Info msg' });
act(() => {
result.current.notifications.warning({ message: 'Warn msg' });
});
expect(mockWarning).toHaveBeenCalledWith({ message: 'Warn msg' });
act(() => {
result.current.notifications.open({ message: 'Open msg' });
});
expect(mockOpen).toHaveBeenCalledWith({ message: 'Open msg' });
});
it('forwards destroy to the underlying API', () => {
const { result } = renderHook(() => useNotifications(), { wrapper });
act(() => {
result.current.notifications.destroy('some-key');
});
expect(mockDestroy).toHaveBeenCalledWith('some-key');
act(() => {
result.current.notifications.destroy();
});
expect(mockDestroy).toHaveBeenCalledWith(undefined);
});
it('accepts React element as message and forwards it', () => {
const { result } = renderHook(() => useNotifications(), { wrapper });
const element = createElement('span', null, 'Custom message');
act(() => {
result.current.notifications.success({ message: element });
});
expect(mockSuccess).toHaveBeenCalledWith({ message: element });
expect(mockSetExtra).not.toHaveBeenCalled();
expect(mockCaptureMessage).not.toHaveBeenCalled();
});
it('accepts empty object and does not break', () => {
const { result } = renderHook(() => useNotifications(), { wrapper });
const emptyPayload = {} as Parameters<
ReturnType<typeof useNotifications>['notifications']['success']
>[0];
act(() => {
result.current.notifications.success(emptyPayload);
});
expect(mockSuccess).toHaveBeenCalledWith(emptyPayload);
expect(mockSetExtra).not.toHaveBeenCalled();
expect(mockCaptureMessage).not.toHaveBeenCalled();
});
});
describe('invalid payloads do not break the app', () => {
const wrapper = ({ children }: { children: ReactNode }) =>
createElement(NotificationProvider, null, children as ReactElement);
it('rejects non-object args and shows fallback', () => {
const { result } = renderHook(() => useNotifications(), { wrapper });
act(() => {
result.current.notifications.success(null as any);
});
expect(mockSetExtra).toHaveBeenCalledWith('notificationArgs', null);
expect(mockCaptureMessage).toHaveBeenCalledWith(
'invalid_args_for_notification',
);
expect(mockSuccess).toHaveBeenCalledWith({
message: 'Error',
description: 'Something went wrong',
});
});
it('rejects undefined and shows fallback', () => {
const { result } = renderHook(() => useNotifications(), { wrapper });
act(() => {
result.current.notifications.error(undefined as any);
});
expect(mockSetExtra).toHaveBeenCalledWith('notificationArgs', undefined);
expect(mockCaptureMessage).toHaveBeenCalledWith(
'invalid_args_for_notification',
);
expect(mockError).toHaveBeenCalledWith({
message: 'Error',
description: 'Something went wrong',
});
});
it('rejects primitive args (string, number) and shows fallback', () => {
const { result } = renderHook(() => useNotifications(), { wrapper });
act(() => {
result.current.notifications.info('just a string' as any);
});
expect(mockSetExtra).toHaveBeenCalledWith(
'notificationArgs',
'just a string',
);
expect(mockCaptureMessage).toHaveBeenCalledWith(
'invalid_args_for_notification',
);
expect(mockInfo).toHaveBeenCalledWith({
message: 'Error',
description: 'Something went wrong',
});
jest.clearAllMocks();
act(() => {
result.current.notifications.warning(42 as any);
});
expect(mockSetExtra).toHaveBeenCalledWith('notificationArgs', 42);
expect(mockCaptureMessage).toHaveBeenCalledWith(
'invalid_args_for_notification',
);
expect(mockWarning).toHaveBeenCalledWith({
message: 'Error',
description: 'Something went wrong',
});
});
it('rejects message as number and shows fallback', () => {
const { result } = renderHook(() => useNotifications(), { wrapper });
act(() => {
result.current.notifications.success({
message: 123 as any,
description: 'Valid description',
});
});
expect(mockSetExtra).toHaveBeenCalledWith('notificationArgs', {
message: 123,
description: 'Valid description',
});
expect(mockCaptureMessage).toHaveBeenCalledWith(
'invalid_args_for_notification',
);
expect(mockSuccess).toHaveBeenCalledWith({
message: 'Error',
description: 'Something went wrong',
});
});
it('rejects description as number and shows fallback', () => {
const { result } = renderHook(() => useNotifications(), { wrapper });
act(() => {
result.current.notifications.error({
message: 'Valid message',
description: 999 as any,
});
});
expect(mockSetExtra).toHaveBeenCalledWith('notificationArgs', {
message: 'Valid message',
description: 999,
});
expect(mockCaptureMessage).toHaveBeenCalledWith(
'invalid_args_for_notification',
);
expect(mockError).toHaveBeenCalledWith({
message: 'Error',
description: 'Something went wrong',
});
});
it('rejects icon as number and shows fallback', () => {
const { result } = renderHook(() => useNotifications(), { wrapper });
act(() => {
result.current.notifications.open({
message: 'Valid',
icon: 0 as any,
});
});
expect(mockSetExtra).toHaveBeenCalledWith('notificationArgs', {
message: 'Valid',
icon: 0,
});
expect(mockCaptureMessage).toHaveBeenCalledWith(
'invalid_args_for_notification',
);
expect(mockOpen).toHaveBeenCalledWith({
message: 'Error',
description: 'Something went wrong',
});
});
it('rejects message as object (non-React element) and shows fallback', () => {
const { result } = renderHook(() => useNotifications(), { wrapper });
act(() => {
result.current.notifications.success({
message: { foo: 'bar' } as any,
});
});
expect(mockSetExtra).toHaveBeenCalledWith('notificationArgs', {
message: { foo: 'bar' },
});
expect(mockCaptureMessage).toHaveBeenCalledWith(
'invalid_args_for_notification',
);
expect(mockSuccess).toHaveBeenCalledWith({
message: 'Error',
description: 'Something went wrong',
});
});
it('invalid payloads never throw', () => {
const { result } = renderHook(() => useNotifications(), { wrapper });
expect(() => {
act(() => {
result.current.notifications.success(null as any);
result.current.notifications.error(undefined as any);
result.current.notifications.info(123 as any);
result.current.notifications.warning({ message: 0 as any });
result.current.notifications.open({
message: 'x',
description: [],
} as any);
});
}).not.toThrow();
});
});
});

View File

@@ -1,75 +1,30 @@
import { isValidElement, useEffect } from 'react';
import * as Sentry from '@sentry/react';
import {
// eslint-disable-next-line no-restricted-imports
createContext,
// eslint-disable-next-line no-restricted-imports
useContext,
useMemo,
} from 'react';
import { notification } from 'antd';
import type { NotificationInstance } from 'antd/es/notification/interface';
import { create } from 'zustand';
import { useShallow } from 'zustand/react/shallow';
type Notification = {
notifications: NotificationInstance;
};
const defaultNotificationInstance: NotificationInstance = {
success: (): void => {},
error: (): void => {},
info: (): void => {},
warning: (): void => {},
open: (): void => {},
destroy: (): void => {},
const defaultNotification: Notification = {
notifications: {
success: (): void => {},
error: (): void => {},
info: (): void => {},
warning: (): void => {},
open: (): void => {},
destroy: (): void => {},
},
};
type NotificationFn = NotificationInstance['success'];
const guardNotificationPayload = (
notificationApi: NotificationInstance,
method: 'success' | 'error' | 'info' | 'warning' | 'open',
): NotificationFn => {
return (notificationArgs): void => {
if (
!notificationArgs ||
typeof notificationArgs !== 'object' ||
(notificationArgs?.message !== undefined &&
typeof notificationArgs.message !== 'string' &&
!isValidElement(notificationArgs.message)) ||
(notificationArgs?.icon !== undefined &&
typeof notificationArgs.icon !== 'string' &&
!isValidElement(notificationArgs.icon)) ||
(notificationArgs?.description !== undefined &&
typeof notificationArgs.description !== 'string' &&
!isValidElement(notificationArgs.description))
) {
Sentry.setExtra('notificationArgs', notificationArgs);
Sentry.captureMessage('invalid_args_for_notification');
return notificationApi[method]({
message: 'Error',
description: 'Something went wrong',
});
}
return notificationApi[method](notificationArgs);
};
};
type NotificationStore = Notification & {
setNotifications: (notifications: NotificationInstance) => void;
};
export const useNotificationStore = create<NotificationStore>(
(set): NotificationStore => ({
notifications: defaultNotificationInstance,
setNotifications: (notifications: NotificationInstance): void =>
set({
notifications: {
success: guardNotificationPayload(notifications, 'success'),
error: guardNotificationPayload(notifications, 'error'),
info: guardNotificationPayload(notifications, 'info'),
warning: guardNotificationPayload(notifications, 'warning'),
open: guardNotificationPayload(notifications, 'open'),
destroy: (key?: string): void => notifications.destroy(key),
},
}),
}),
export const NotificationContext = createContext<Notification>(
defaultNotification,
);
export function NotificationProvider({
@@ -78,21 +33,17 @@ export function NotificationProvider({
children: JSX.Element;
}): JSX.Element {
const [notificationApi, NotificationElement] = notification.useNotification();
const setNotifications = useNotificationStore((s) => s.setNotifications);
useEffect(() => {
setNotifications(notificationApi);
}, [notificationApi, setNotifications]);
const notifications = useMemo(() => ({ notifications: notificationApi }), [
notificationApi,
]);
return (
<>
<NotificationContext.Provider value={notifications}>
{NotificationElement}
{children}
</>
</NotificationContext.Provider>
);
}
export const useNotifications = (): Notification =>
useNotificationStore(
useShallow((state) => ({ notifications: state.notifications })),
);
useContext(NotificationContext);

View File

@@ -3,10 +3,10 @@ import { ErrorStatusCode, SuccessStatusCode } from 'types/common';
export type ApiResponse<T> = { data: T };
export interface ErrorResponse<ErrorObject = string> {
export interface ErrorResponse {
statusCode: ErrorStatusCode;
payload: null;
error: ErrorObject;
error: string;
message: string | null;
body?: string | null;
}