Compare commits

..

17 Commits

Author SHA1 Message Date
SagarRajput-7
d887b4a67e chore: base path setup and routing and navigation migrations 2026-04-15 17:56:37 +05:30
SagarRajput-7
c88c7413ac feat: set React Router basename from <base href> tag 2026-04-15 17:56:37 +05:30
SagarRajput-7
622ccf6742 test: add getBasePath() contract test; mark basePath export as internal 2026-04-15 17:56:37 +05:30
SagarRajput-7
21821fde57 feat: add getBasePath() utility reading <base href> from DOM 2026-04-15 17:56:37 +05:30
SagarRajput-7
7208777ca6 chore: public dir cleanup, remove duplicate assets since they are moved to src/assets 2026-04-15 17:43:10 +05:30
SagarRajput-7
ad3e0527ce chore: addressed comments 2026-04-15 03:04:55 +05:30
SagarRajput-7
555dc5881e Merge branch 'main' into asset-consumption 2026-04-15 02:38:27 +05:30
SagarRajput-7
4761fcb57a chore: addressed comments and added public reference rule 2026-04-15 02:37:34 +05:30
SagarRajput-7
bda93ee286 Merge branch 'main' into asset-consumption 2026-04-14 16:55:41 +05:30
SagarRajput-7
7edc737076 chore: addressed feedback comments 2026-04-14 16:54:56 +05:30
SagarRajput-7
4fa306f94f chore: strengthen the lint rule and migration the gaps found 2026-04-14 16:11:21 +05:30
SagarRajput-7
c415b2a1d7 chore: fmt fix 2026-04-14 15:16:46 +05:30
SagarRajput-7
d513188511 chore: code refactor 2026-04-14 15:16:46 +05:30
SagarRajput-7
e7edc5da82 chore: asset migration - using the src/assets across the codebase, instead of the public dir 2026-04-14 15:16:46 +05:30
SagarRajput-7
dae7a43a52 chore: added eslint and stylelint for the asset migration task 2026-04-14 15:16:46 +05:30
SagarRajput-7
9fb373e36b chore: remove the snapshot update since no migration is done under this pr 2026-04-14 15:16:46 +05:30
SagarRajput-7
b01d4d8a74 chore: jest module resolution fix 2026-04-14 15:16:46 +05:30
101 changed files with 718 additions and 5923 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.119.0
image: signoz/signoz:v0.118.0
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port

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.119.0
image: signoz/signoz:v0.118.0
ports:
- "8080:8080" # signoz port
volumes:

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.119.0}
image: signoz/signoz:${VERSION:-v0.118.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

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.119.0}
image: signoz/signoz:${VERSION:-v0.118.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import (
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/middleware"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
@@ -48,6 +49,7 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.
CloudIntegrationsController: opts.CloudIntegrationsController,
LogsParsingPipelineController: opts.LogsParsingPipelineController,
FluxInterval: opts.FluxInterval,
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
Signoz: signoz,
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),

View File

@@ -213,6 +213,23 @@ module.exports = {
message:
'Avoid calling .getState() directly. Export a standalone action from the store instead.',
},
{
selector:
"CallExpression[callee.object.name='window'][callee.property.name='open']",
message:
'Do not call window.open() directly. ' +
"Use openInNewTab() from 'utils/navigation' for internal SigNoz paths. " +
"For intentional external URLs, use openExternalLink() from 'utils/navigation'. " +
'For unavoidable direct calls, add // eslint-disable-next-line with a reason.',
},
{
selector:
"AssignmentExpression[left.object.name='window'][left.property.name='href']",
message:
'Do not assign window.location.href for internal navigation. ' +
"Use history.push() or history.replace() from 'lib/history'. " +
'For external redirects (SSO, logout URLs), add // eslint-disable-next-line with a reason.',
},
],
},
overrides: [
@@ -265,5 +282,14 @@ module.exports = {
'no-restricted-syntax': 'off',
},
},
{
// navigation.ts and useSafeNavigate.ts are the canonical implementations that call
// window.open after computing a base-path-aware href. They are the only places
// allowed to call window.open directly.
files: ['src/utils/navigation.ts', 'src/hooks/useSafeNavigate.ts'],
rules: {
'no-restricted-syntax': 'off',
},
},
],
};

View File

@@ -6,3 +6,6 @@ VITE_APPCUES_APP_ID="appcess-app-id"
VITE_PYLON_IDENTITY_SECRET="pylon-identity-secret"
CI="1"
# Uncomment to test sub-path deployment locally (e.g. app served at /signoz/).
# VITE_BASE_PATH="/signoz/"

View File

@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="<%- BASE_PATH %>" />
<meta charset="utf-8" />
<meta
http-equiv="Cache-Control"
@@ -59,7 +60,7 @@
<meta data-react-helmet="true" name="docusaurus_locale" content="en" />
<meta data-react-helmet="true" name="docusaurus_tag" content="default" />
<meta name="robots" content="noindex" />
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
<link data-react-helmet="true" rel="shortcut icon" href="./favicon.ico" />
</head>
<body data-theme="default">
<noscript>You need to enable JavaScript to run this app.</noscript>
@@ -113,7 +114,7 @@
})(document, 'script');
}
</script>
<link rel="stylesheet" href="/css/uPlot.min.css" />
<link rel="stylesheet" href="./css/uPlot.min.css" />
<script type="module" src="./src/index.tsx"></script>
</body>
</html>

View File

@@ -20,6 +20,7 @@ const config: Config.InitialOptions = {
'^@signozhq/resizable$': '<rootDir>/__mocks__/resizableMock.tsx',
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^hooks/useSafeNavigate\\.impl$': '<rootDir>/src/hooks/useSafeNavigate.ts',
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^constants/env$': '<rootDir>/__mocks__/env.ts',
'^src/constants/env$': '<rootDir>/__mocks__/env.ts',

View File

@@ -1,97 +0,0 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
InvalidateOptions,
QueryClient,
QueryFunction,
QueryKey,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useQuery } from 'react-query';
import type { ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { GetAlerts200, RenderErrorResponseDTO } from '../sigNoz.schemas';
/**
* This endpoint returns alerts for the organization
* @summary Get alerts
*/
export const getAlerts = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetAlerts200>({
url: `/api/v1/alerts`,
method: 'GET',
signal,
});
};
export const getGetAlertsQueryKey = () => {
return [`/api/v1/alerts`] as const;
};
export const getGetAlertsQueryOptions = <
TData = Awaited<ReturnType<typeof getAlerts>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getAlerts>>, TError, TData>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetAlertsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getAlerts>>> = ({
signal,
}) => getAlerts(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getAlerts>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetAlertsQueryResult = NonNullable<
Awaited<ReturnType<typeof getAlerts>>
>;
export type GetAlertsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get alerts
*/
export function useGetAlerts<
TData = Awaited<ReturnType<typeof getAlerts>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getAlerts>>, TError, TData>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetAlertsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get alerts
*/
export const invalidateGetAlerts = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetAlertsQueryKey() },
options,
);
return queryClient;
};

View File

@@ -1,646 +0,0 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useMutation, useQuery } from 'react-query';
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
ConfigReceiverDTO,
CreateChannel201,
DeleteChannelByIDPathParameters,
GetChannelByID200,
GetChannelByIDPathParameters,
ListChannels200,
RenderErrorResponseDTO,
UpdateChannelByIDPathParameters,
} from '../sigNoz.schemas';
/**
* This endpoint lists all notification channels for the organization
* @summary List notification channels
*/
export const listChannels = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListChannels200>({
url: `/api/v1/channels`,
method: 'GET',
signal,
});
};
export const getListChannelsQueryKey = () => {
return [`/api/v1/channels`] as const;
};
export const getListChannelsQueryOptions = <
TData = Awaited<ReturnType<typeof listChannels>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listChannels>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListChannelsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listChannels>>> = ({
signal,
}) => listChannels(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listChannels>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListChannelsQueryResult = NonNullable<
Awaited<ReturnType<typeof listChannels>>
>;
export type ListChannelsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List notification channels
*/
export function useListChannels<
TData = Awaited<ReturnType<typeof listChannels>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listChannels>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListChannelsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary List notification channels
*/
export const invalidateListChannels = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListChannelsQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint creates a notification channel
* @summary Create notification channel
*/
export const createChannel = (
configReceiverDTO: BodyType<ConfigReceiverDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateChannel201>({
url: `/api/v1/channels`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: configReceiverDTO,
signal,
});
};
export const getCreateChannelMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationKey = ['createChannel'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createChannel>>,
{ data: BodyType<ConfigReceiverDTO> }
> = (props) => {
const { data } = props ?? {};
return createChannel(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateChannelMutationResult = NonNullable<
Awaited<ReturnType<typeof createChannel>>
>;
export type CreateChannelMutationBody = BodyType<ConfigReceiverDTO>;
export type CreateChannelMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create notification channel
*/
export const useCreateChannel = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationOptions = getCreateChannelMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint deletes a notification channel by ID
* @summary Delete notification channel
*/
export const deleteChannelByID = ({ id }: DeleteChannelByIDPathParameters) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/channels/${id}`,
method: 'DELETE',
});
};
export const getDeleteChannelByIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteChannelByID>>,
TError,
{ pathParams: DeleteChannelByIDPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteChannelByID>>,
TError,
{ pathParams: DeleteChannelByIDPathParameters },
TContext
> => {
const mutationKey = ['deleteChannelByID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteChannelByID>>,
{ pathParams: DeleteChannelByIDPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteChannelByID(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteChannelByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteChannelByID>>
>;
export type DeleteChannelByIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete notification channel
*/
export const useDeleteChannelByID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteChannelByID>>,
TError,
{ pathParams: DeleteChannelByIDPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteChannelByID>>,
TError,
{ pathParams: DeleteChannelByIDPathParameters },
TContext
> => {
const mutationOptions = getDeleteChannelByIDMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint returns a notification channel by ID
* @summary Get notification channel by ID
*/
export const getChannelByID = (
{ id }: GetChannelByIDPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetChannelByID200>({
url: `/api/v1/channels/${id}`,
method: 'GET',
signal,
});
};
export const getGetChannelByIDQueryKey = ({
id,
}: GetChannelByIDPathParameters) => {
return [`/api/v1/channels/${id}`] as const;
};
export const getGetChannelByIDQueryOptions = <
TData = Awaited<ReturnType<typeof getChannelByID>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetChannelByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getChannelByID>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetChannelByIDQueryKey({ id });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getChannelByID>>> = ({
signal,
}) => getChannelByID({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getChannelByID>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetChannelByIDQueryResult = NonNullable<
Awaited<ReturnType<typeof getChannelByID>>
>;
export type GetChannelByIDQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get notification channel by ID
*/
export function useGetChannelByID<
TData = Awaited<ReturnType<typeof getChannelByID>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetChannelByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getChannelByID>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetChannelByIDQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get notification channel by ID
*/
export const invalidateGetChannelByID = async (
queryClient: QueryClient,
{ id }: GetChannelByIDPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetChannelByIDQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint updates a notification channel by ID
* @summary Update notification channel
*/
export const updateChannelByID = (
{ id }: UpdateChannelByIDPathParameters,
configReceiverDTO: BodyType<ConfigReceiverDTO>,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/channels/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: configReceiverDTO,
});
};
export const getUpdateChannelByIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateChannelByID>>,
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data: BodyType<ConfigReceiverDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateChannelByID>>,
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data: BodyType<ConfigReceiverDTO>;
},
TContext
> => {
const mutationKey = ['updateChannelByID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateChannelByID>>,
{
pathParams: UpdateChannelByIDPathParameters;
data: BodyType<ConfigReceiverDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateChannelByID(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateChannelByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof updateChannelByID>>
>;
export type UpdateChannelByIDMutationBody = BodyType<ConfigReceiverDTO>;
export type UpdateChannelByIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update notification channel
*/
export const useUpdateChannelByID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateChannelByID>>,
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data: BodyType<ConfigReceiverDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateChannelByID>>,
TError,
{
pathParams: UpdateChannelByIDPathParameters;
data: BodyType<ConfigReceiverDTO>;
},
TContext
> => {
const mutationOptions = getUpdateChannelByIDMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint tests a notification channel by sending a test notification
* @summary Test notification channel
*/
export const testChannel = (
configReceiverDTO: BodyType<ConfigReceiverDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/channels/test`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: configReceiverDTO,
signal,
});
};
export const getTestChannelMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationKey = ['testChannel'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof testChannel>>,
{ data: BodyType<ConfigReceiverDTO> }
> = (props) => {
const { data } = props ?? {};
return testChannel(data);
};
return { mutationFn, ...mutationOptions };
};
export type TestChannelMutationResult = NonNullable<
Awaited<ReturnType<typeof testChannel>>
>;
export type TestChannelMutationBody = BodyType<ConfigReceiverDTO>;
export type TestChannelMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Test notification channel
*/
export const useTestChannel = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof testChannel>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationOptions = getTestChannelMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Deprecated: use /api/v1/channels/test instead
* @deprecated
* @summary Test notification channel (deprecated)
*/
export const testChannelDeprecated = (
configReceiverDTO: BodyType<ConfigReceiverDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/testChannel`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: configReceiverDTO,
signal,
});
};
export const getTestChannelDeprecatedMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationKey = ['testChannelDeprecated'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof testChannelDeprecated>>,
{ data: BodyType<ConfigReceiverDTO> }
> = (props) => {
const { data } = props ?? {};
return testChannelDeprecated(data);
};
return { mutationFn, ...mutationOptions };
};
export type TestChannelDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof testChannelDeprecated>>
>;
export type TestChannelDeprecatedMutationBody = BodyType<ConfigReceiverDTO>;
export type TestChannelDeprecatedMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Test notification channel (deprecated)
*/
export const useTestChannelDeprecated = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof testChannelDeprecated>>,
TError,
{ data: BodyType<ConfigReceiverDTO> },
TContext
> => {
const mutationOptions = getTestChannelDeprecatedMutationOptions(options);
return useMutation(mutationOptions);
};

View File

@@ -1,482 +0,0 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useMutation, useQuery } from 'react-query';
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
AlertmanagertypesPostableRoutePolicyDTO,
CreateRoutePolicy201,
DeleteRoutePolicyByIDPathParameters,
GetAllRoutePolicies200,
GetRoutePolicyByID200,
GetRoutePolicyByIDPathParameters,
RenderErrorResponseDTO,
UpdateRoutePolicy200,
UpdateRoutePolicyPathParameters,
} from '../sigNoz.schemas';
/**
* This endpoint lists all route policies for the organization
* @summary List route policies
*/
export const getAllRoutePolicies = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetAllRoutePolicies200>({
url: `/api/v1/route_policies`,
method: 'GET',
signal,
});
};
export const getGetAllRoutePoliciesQueryKey = () => {
return [`/api/v1/route_policies`] as const;
};
export const getGetAllRoutePoliciesQueryOptions = <
TData = Awaited<ReturnType<typeof getAllRoutePolicies>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAllRoutePolicies>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetAllRoutePoliciesQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getAllRoutePolicies>>
> = ({ signal }) => getAllRoutePolicies(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getAllRoutePolicies>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetAllRoutePoliciesQueryResult = NonNullable<
Awaited<ReturnType<typeof getAllRoutePolicies>>
>;
export type GetAllRoutePoliciesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List route policies
*/
export function useGetAllRoutePolicies<
TData = Awaited<ReturnType<typeof getAllRoutePolicies>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAllRoutePolicies>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetAllRoutePoliciesQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary List route policies
*/
export const invalidateGetAllRoutePolicies = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetAllRoutePoliciesQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint creates a route policy
* @summary Create route policy
*/
export const createRoutePolicy = (
alertmanagertypesPostableRoutePolicyDTO: BodyType<AlertmanagertypesPostableRoutePolicyDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateRoutePolicy201>({
url: `/api/v1/route_policies`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: alertmanagertypesPostableRoutePolicyDTO,
signal,
});
};
export const getCreateRoutePolicyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRoutePolicy>>,
TError,
{ data: BodyType<AlertmanagertypesPostableRoutePolicyDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createRoutePolicy>>,
TError,
{ data: BodyType<AlertmanagertypesPostableRoutePolicyDTO> },
TContext
> => {
const mutationKey = ['createRoutePolicy'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createRoutePolicy>>,
{ data: BodyType<AlertmanagertypesPostableRoutePolicyDTO> }
> = (props) => {
const { data } = props ?? {};
return createRoutePolicy(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateRoutePolicyMutationResult = NonNullable<
Awaited<ReturnType<typeof createRoutePolicy>>
>;
export type CreateRoutePolicyMutationBody = BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
export type CreateRoutePolicyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create route policy
*/
export const useCreateRoutePolicy = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRoutePolicy>>,
TError,
{ data: BodyType<AlertmanagertypesPostableRoutePolicyDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createRoutePolicy>>,
TError,
{ data: BodyType<AlertmanagertypesPostableRoutePolicyDTO> },
TContext
> => {
const mutationOptions = getCreateRoutePolicyMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint deletes a route policy by ID
* @summary Delete route policy
*/
export const deleteRoutePolicyByID = ({
id,
}: DeleteRoutePolicyByIDPathParameters) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/route_policies/${id}`,
method: 'DELETE',
});
};
export const getDeleteRoutePolicyByIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>,
TError,
{ pathParams: DeleteRoutePolicyByIDPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>,
TError,
{ pathParams: DeleteRoutePolicyByIDPathParameters },
TContext
> => {
const mutationKey = ['deleteRoutePolicyByID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>,
{ pathParams: DeleteRoutePolicyByIDPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteRoutePolicyByID(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteRoutePolicyByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>
>;
export type DeleteRoutePolicyByIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete route policy
*/
export const useDeleteRoutePolicyByID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>,
TError,
{ pathParams: DeleteRoutePolicyByIDPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteRoutePolicyByID>>,
TError,
{ pathParams: DeleteRoutePolicyByIDPathParameters },
TContext
> => {
const mutationOptions = getDeleteRoutePolicyByIDMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint returns a route policy by ID
* @summary Get route policy by ID
*/
export const getRoutePolicyByID = (
{ id }: GetRoutePolicyByIDPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRoutePolicyByID200>({
url: `/api/v1/route_policies/${id}`,
method: 'GET',
signal,
});
};
export const getGetRoutePolicyByIDQueryKey = ({
id,
}: GetRoutePolicyByIDPathParameters) => {
return [`/api/v1/route_policies/${id}`] as const;
};
export const getGetRoutePolicyByIDQueryOptions = <
TData = Awaited<ReturnType<typeof getRoutePolicyByID>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRoutePolicyByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRoutePolicyByID>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetRoutePolicyByIDQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRoutePolicyByID>>
> = ({ signal }) => getRoutePolicyByID({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRoutePolicyByID>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRoutePolicyByIDQueryResult = NonNullable<
Awaited<ReturnType<typeof getRoutePolicyByID>>
>;
export type GetRoutePolicyByIDQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get route policy by ID
*/
export function useGetRoutePolicyByID<
TData = Awaited<ReturnType<typeof getRoutePolicyByID>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRoutePolicyByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRoutePolicyByID>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRoutePolicyByIDQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get route policy by ID
*/
export const invalidateGetRoutePolicyByID = async (
queryClient: QueryClient,
{ id }: GetRoutePolicyByIDPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRoutePolicyByIDQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint updates a route policy by ID
* @summary Update route policy
*/
export const updateRoutePolicy = (
{ id }: UpdateRoutePolicyPathParameters,
alertmanagertypesPostableRoutePolicyDTO: BodyType<AlertmanagertypesPostableRoutePolicyDTO>,
) => {
return GeneratedAPIInstance<UpdateRoutePolicy200>({
url: `/api/v1/route_policies/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: alertmanagertypesPostableRoutePolicyDTO,
});
};
export const getUpdateRoutePolicyMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateRoutePolicy>>,
TError,
{
pathParams: UpdateRoutePolicyPathParameters;
data: BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateRoutePolicy>>,
TError,
{
pathParams: UpdateRoutePolicyPathParameters;
data: BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
},
TContext
> => {
const mutationKey = ['updateRoutePolicy'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateRoutePolicy>>,
{
pathParams: UpdateRoutePolicyPathParameters;
data: BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateRoutePolicy(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateRoutePolicyMutationResult = NonNullable<
Awaited<ReturnType<typeof updateRoutePolicy>>
>;
export type UpdateRoutePolicyMutationBody = BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
export type UpdateRoutePolicyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update route policy
*/
export const useUpdateRoutePolicy = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateRoutePolicy>>,
TError,
{
pathParams: UpdateRoutePolicyPathParameters;
data: BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateRoutePolicy>>,
TError,
{
pathParams: UpdateRoutePolicyPathParameters;
data: BodyType<AlertmanagertypesPostableRoutePolicyDTO>;
},
TContext
> => {
const mutationOptions = getUpdateRoutePolicyMutationOptions(options);
return useMutation(mutationOptions);
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { Button } from '@signozhq/button';
import { LifeBuoy } from 'lucide-react';
import { openExternalLink } from 'utils/navigation';
import signozBrandLogoUrl from '@/assets/Logos/signoz-brand-logo.svg';
@@ -8,7 +9,7 @@ import './AuthHeader.styles.scss';
function AuthHeader(): JSX.Element {
const handleGetHelp = useCallback((): void => {
window.open('https://signoz.io/support/', '_blank');
openExternalLink('https://signoz.io/support/');
}, []);
return (

View File

@@ -7,11 +7,13 @@ import ROUTES from 'constants/routes';
import useUpdatedQuery from 'container/GridCardLayout/useResolveQuery';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { AppState } from 'store/reducers';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
export interface NavigateToExplorerProps {
filters: TagFilterItem[];
@@ -133,7 +135,11 @@ export function useNavigateToExplorer(): (
QueryParams.compositeQuery
}=${JSONCompositeQuery}`;
window.open(newExplorerPath, sameTab ? '_self' : '_blank');
if (sameTab) {
history.push(newExplorerPath);
} else {
openInNewTab(newExplorerPath);
}
},
[
prepareQuery,

View File

@@ -11,6 +11,7 @@ import { ChevronsDown, ScrollText } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { ChangelogSchema } from 'types/api/changelog/getChangelogByVersion';
import { UserPreference } from 'types/api/preferences/preference';
import { openExternalLink } from 'utils/navigation';
import ChangelogRenderer from './components/ChangelogRenderer';
@@ -86,11 +87,7 @@ function ChangelogModal({ changelog, onClose }: Props): JSX.Element {
}, [checkScroll]);
const onClickUpdateWorkspace = (): void => {
window.open(
'https://signoz.io/upgrade-path',
'_blank',
'noopener,noreferrer',
);
openExternalLink('https://signoz.io/upgrade-path');
};
const onClickScrollForMore = (): void => {

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import { VIEW_TYPES, VIEWS } from './constants';
@@ -330,10 +331,7 @@ function HostMetricsDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -352,10 +350,7 @@ function HostMetricsDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -1,6 +1,7 @@
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import { ArrowUpRight } from 'lucide-react';
import { openExternalLink } from 'utils/navigation';
import './LearnMore.styles.scss';
@@ -14,7 +15,7 @@ function LearnMore({ text, url, onClick }: LearnMoreProps): JSX.Element {
const handleClick = (): void => {
onClick?.();
if (url) {
window.open(url, '_blank');
openExternalLink(url);
}
};
return (

View File

@@ -20,6 +20,7 @@ import {
KAFKA_SETUP_DOC_LINK,
MessagingQueueHealthCheckService,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { openExternalLink } from 'utils/navigation';
import { v4 as uuid } from 'uuid';
import './MessagingQueueHealthCheck.styles.scss';
@@ -76,7 +77,7 @@ function ErrorTitleAndKey({
if (isCloudUserVal && !!link) {
history.push(link);
} else {
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
openExternalLink(KAFKA_SETUP_DOC_LINK);
}
};
return {

View File

@@ -2,6 +2,7 @@ import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import EmptyQuickFilterIcon from 'assets/CustomIcons/EmptyQuickFilterIcon';
import { ArrowUpRight } from 'lucide-react';
import { openExternalLink } from 'utils/navigation';
const QUICK_FILTER_DOC_PATHS: Record<string, string> = {
severity_text: 'severity-text',
@@ -22,9 +23,8 @@ function LogsQuickFilterEmptyState({
const handleLearnMoreClick = (): void => {
const section = QUICK_FILTER_DOC_PATHS[attributeKey];
window.open(
openExternalLink(
`https://signoz.io/docs/logs-management/features/logs-quick-filters#${section}`,
'_blank',
);
};
return (

View File

@@ -9,6 +9,7 @@ import {
} from 'container/ApiMonitoring/utils';
import { UnfoldVertical } from 'lucide-react';
import { SuccessResponse } from 'types/api';
import { openInNewTab } from 'utils/navigation';
import emptyStateUrl from '@/assets/Icons/emptyState.svg';
@@ -107,7 +108,7 @@ function DependentServices({
urlQuery.set(QueryParams.startTime, timeRange.startTime.toString());
urlQuery.set(QueryParams.endTime, timeRange.endTime.toString());
url.search = urlQuery.toString();
window.open(url.toString(), '_blank');
openInNewTab(`${url.pathname}${url.search}`);
},
className: 'clickable-row',
})}

View File

@@ -7,6 +7,7 @@ import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { isModifierKeyPressed } from 'utils/app';
import { openExternalLink } from 'utils/navigation';
import { getOptionList } from './config';
import { AlertTypeCard, SelectTypeContainer } from './styles';
@@ -55,7 +56,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
page: 'New alert data source selection page',
});
window.open(url, '_blank');
openExternalLink(url);
}
const renderOptions = useMemo(
() => (

View File

@@ -14,6 +14,7 @@ import { IUser } from 'providers/App/types';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { USER_ROLES } from 'types/roles';
import { openInNewTab } from 'utils/navigation';
import { ROUTING_POLICIES_ROUTE } from './constants';
import { RoutingPolicyBannerProps } from './types';
@@ -387,7 +388,7 @@ export function NotificationChannelsNotFoundContent({
style={{ padding: '0 4px' }}
type="link"
onClick={(): void => {
window.open(ROUTES.CHANNELS_NEW, '_blank');
openInNewTab(ROUTES.CHANNELS_NEW);
}}
>
here.

View File

@@ -21,6 +21,7 @@ import { useGetHosts, usePutHost } from 'api/generated/services/zeus';
import { AxiosError } from 'axios';
import { useAppContext } from 'providers/App/App';
import { useTimezone } from 'providers/Timezone';
import { openExternalLink } from 'utils/navigation';
import CustomDomainEditModal from './CustomDomainEditModal';
@@ -48,7 +49,7 @@ function DomainUpdateToast({
className="custom-domain-toast-visit-btn"
suffixIcon={<ExternalLink size={12} />}
onClick={(): void => {
window.open(url, '_blank', 'noopener,noreferrer');
openExternalLink(url);
}}
>
Visit new URL

View File

@@ -16,6 +16,7 @@ import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
import APIError from 'types/api/error';
import { USER_ROLES } from 'types/roles';
import { openInNewTab } from 'utils/navigation';
import './PublicDashboard.styles.scss';
@@ -294,7 +295,7 @@ function PublicDashboardSetting(): JSX.Element {
icon={<ExternalLink size={12} />}
onClick={(): void => {
if (publicDashboardURL) {
window.open(publicDashboardURL, '_blank');
openInNewTab(publicDashboardURL);
}
}}
/>

View File

@@ -15,6 +15,7 @@ import { AlertDef, Labels } from 'types/api/alerts/def';
import { Channels } from 'types/api/channels/getAll';
import APIError from 'types/api/error';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
import { openInNewTab } from 'utils/navigation';
import { popupContainer } from 'utils/selectPopupContainer';
import ChannelSelect from './ChannelSelect';
@@ -87,7 +88,7 @@ function BasicInfo({
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
ruleId: isNewRule ? 0 : alertDef?.id,
});
window.open(ROUTES.CHANNELS_NEW, '_blank');
openInNewTab(ROUTES.CHANNELS_NEW);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const hasLoggedEvent = useRef(false);

View File

@@ -46,6 +46,7 @@ import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed } from 'utils/app';
import { compositeQueryToQueryEnvelope } from 'utils/compositeQueryToQueryEnvelope';
import { openExternalLink } from 'utils/navigation';
import BasicInfo from './BasicInfo';
import ChartPreview from './ChartPreview';
@@ -771,7 +772,7 @@ function FormAlertRules({
queryType: currentQuery.queryType,
link: url,
});
window.open(url, '_blank');
openExternalLink(url);
}
}

View File

@@ -13,6 +13,7 @@ import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { GettableAlert } from 'types/api/alerts/get';
import { USER_ROLES } from 'types/roles';
import { openExternalLink } from 'utils/navigation';
import beaconUrl from '@/assets/Icons/beacon.svg';
@@ -103,11 +104,7 @@ export default function AlertRules({
source: 'Alert Rules',
});
window.open(
'https://signoz.io/docs/alerts/',
'_blank',
'noreferrer noopener',
);
openExternalLink('https://signoz.io/docs/alerts/');
}}
>
Learn more <ArrowUpRight size={12} />

View File

@@ -10,6 +10,7 @@ import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { Dashboard } from 'types/api/dashboard/getAll';
import { USER_ROLES } from 'types/roles';
import { openExternalLink, openInNewTab } from 'utils/navigation';
import dialsUrl from '@/assets/Icons/dials.svg';
@@ -87,10 +88,7 @@ export default function Dashboards({
logEvent('Homepage: Learn more clicked', {
source: 'Dashboards',
});
window.open(
'https://signoz.io/docs/userguide/manage-dashboards/',
'_blank',
);
openExternalLink('https://signoz.io/docs/userguide/manage-dashboards/');
}}
>
Learn more <ArrowUpRight size={12} />
@@ -114,7 +112,7 @@ export default function Dashboards({
dashboardName: dashboard.data.title,
});
if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank');
openInNewTab(getLink());
} else {
safeNavigate(getLink());
}

View File

@@ -9,6 +9,7 @@ import { Link2 } from 'lucide-react';
import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { openExternalLink } from 'utils/navigation';
import containerPlusUrl from '@/assets/Icons/container-plus.svg';
import helloWaveUrl from '@/assets/Icons/hello-wave.svg';
@@ -51,7 +52,7 @@ function DataSourceInfo({
if (activeLicense && activeLicense.platform === LicensePlatform.CLOUD) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(DOCS_LINKS.ADD_DATA_SOURCE, '_blank', 'noopener noreferrer');
openExternalLink(DOCS_LINKS.ADD_DATA_SOURCE);
}
};

View File

@@ -8,6 +8,7 @@ import { ArrowRight, ArrowRightToLine, BookOpenText } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { USER_ROLES } from 'types/roles';
import { openExternalLink } from 'utils/navigation';
import './HomeChecklist.styles.scss';
@@ -99,11 +100,7 @@ function HomeChecklist({
) {
history.push(item.toRoute || '');
} else {
window?.open(
item.docsLink || '',
'_blank',
'noopener noreferrer',
);
openExternalLink(item.docsLink || '');
}
}}
>
@@ -119,7 +116,7 @@ function HomeChecklist({
step: item.id,
});
window?.open(item.docsLink, '_blank', 'noopener noreferrer');
openExternalLink(item.docsLink || '');
}}
>
<BookOpenText size={16} />

View File

@@ -19,12 +19,13 @@ import { useAppContext } from 'providers/App/App';
import { ViewProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
import { USER_ROLES } from 'types/roles';
import { openExternalLink } from 'utils/navigation';
import circusTentUrl from '@/assets/Icons/circus-tent.svg';
import eightBallUrl from '@/assets/Icons/eight-ball.svg';
import floppyDiscUrl from '@/assets/Icons/floppy-disc.svg';
import logsUrl from '@/assets/Icons/logs.svg';
import { getItemIcon } from '../constants';
export default function SavedViews({
onUpdateChecklistDoneItem,
loadingUserPreferences,
@@ -196,11 +197,7 @@ export default function SavedViews({
entity: selectedEntity,
});
window.open(
'https://signoz.io/docs/product-features/saved-view/',
'_blank',
'noopener noreferrer',
);
openExternalLink('https://signoz.io/docs/product-features/saved-view/');
}}
>
Learn more <ArrowUpRight size={12} />
@@ -229,7 +226,7 @@ export default function SavedViews({
>
<div className="saved-view-item-name-container home-data-item-name-container">
<img
src={getItemIcon(String(view.id))}
src={view.id % 2 === 0 ? eightBallUrl : circusTentUrl}
alt="alert-rules"
className="alert-rules-img"
/>

View File

@@ -31,6 +31,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { USER_ROLES } from 'types/roles';
import { isModifierKeyPressed } from 'utils/app';
import { openExternalLink } from 'utils/navigation';
import triangleRulerUrl from '@/assets/Icons/triangle-ruler.svg';
@@ -79,11 +80,7 @@ const EmptyState = memo(
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',
);
openExternalLink(DOCS_LINKS.ADD_DATA_SOURCE);
}
}}
>
@@ -97,10 +94,7 @@ const EmptyState = memo(
logEvent('Homepage: Learn more clicked', {
source: 'Service Metrics',
});
window.open(
'https://signoz.io/docs/instrumentation/overview/',
'_blank',
);
openExternalLink('https://signoz.io/docs/instrumentation/overview/');
}}
>
Learn more <ArrowUpRight size={12} />

View File

@@ -17,6 +17,7 @@ import { ServicesList } from 'types/api/metrics/getService';
import { GlobalReducer } from 'types/reducer/globalTime';
import { USER_ROLES } from 'types/roles';
import { isModifierKeyPressed } from 'utils/app';
import { openExternalLink } from 'utils/navigation';
import triangleRulerUrl from '@/assets/Icons/triangle-ruler.svg';
@@ -133,11 +134,7 @@ export default function ServiceTraces({
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',
);
openExternalLink(DOCS_LINKS.ADD_DATA_SOURCE);
}
}}
>
@@ -151,10 +148,7 @@ export default function ServiceTraces({
logEvent('Homepage: Learn more clicked', {
source: 'Service Traces',
});
window.open(
'https://signoz.io/docs/instrumentation/overview/',
'_blank',
);
openExternalLink('https://signoz.io/docs/instrumentation/overview/');
}}
>
Learn more <ArrowUpRight size={12} />

View File

@@ -49,6 +49,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import ClusterEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -414,10 +415,7 @@ function ClusterDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -436,10 +434,7 @@ function ClusterDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -48,6 +48,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import DaemonSetEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -429,10 +430,7 @@ function DaemonSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -451,10 +449,7 @@ function DaemonSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import DeploymentEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -433,10 +434,7 @@ function DeploymentDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -455,10 +453,7 @@ function DeploymentDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -48,6 +48,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import JobEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -427,10 +428,7 @@ function JobDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -449,10 +447,7 @@ function JobDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import NamespaceEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -419,10 +420,7 @@ function NamespaceDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -441,10 +439,7 @@ function NamespaceDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import NodeLogs from '../../EntityDetailsUtils/EntityLogs';
@@ -416,10 +417,7 @@ function NodeDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -438,10 +436,7 @@ function NodeDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import PodEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -435,10 +436,7 @@ function PodDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -457,10 +455,7 @@ function PodDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -53,6 +53,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import {
@@ -431,10 +432,7 @@ function StatefulSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -453,10 +451,7 @@ function StatefulSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -1,5 +1,6 @@
import { ArrowRightOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import { openExternalLink } from 'utils/navigation';
interface AlertInfoCardProps {
header: string;
@@ -19,7 +20,7 @@ function AlertInfoCard({
className="alert-info-card"
onClick={(): void => {
onClick();
window.open(link, '_blank');
openExternalLink(link);
}}
>
<div className="alert-card-text">

View File

@@ -1,5 +1,6 @@
import { ArrowRightOutlined, PlayCircleFilled } from '@ant-design/icons';
import { Flex, Typography } from 'antd';
import { openExternalLink } from 'utils/navigation';
interface InfoLinkTextProps {
infoText: string;
@@ -20,7 +21,7 @@ function InfoLinkText({
<Flex
onClick={(): void => {
onClick();
window.open(link, '_blank');
openExternalLink(link);
}}
className="info-link-container"
>

View File

@@ -83,6 +83,7 @@ import {
} from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { isModifierKeyPressed } from 'utils/app';
import { openExternalLink, openInNewTab } from 'utils/navigation';
import awwSnapUrl from '@/assets/Icons/awwSnap.svg';
import dashboardsUrl from '@/assets/Icons/dashboards.svg';
@@ -457,7 +458,7 @@ function DashboardsList(): JSX.Element {
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
window.open(getLink(), '_blank');
openInNewTab(getLink());
}}
>
Open in New Tab
@@ -739,9 +740,8 @@ function DashboardsList(): JSX.Element {
className="learn-more"
data-testid="learn-more"
onClick={(): void => {
window.open(
openExternalLink(
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state',
'_blank',
);
}}
>

View File

@@ -1,6 +1,7 @@
import { LockFilled } from '@ant-design/icons';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { openInNewTab } from 'utils/navigation';
import { Data } from '../DashboardsList';
import { TableLinkText } from './styles';
@@ -12,7 +13,7 @@ function Name(name: Data['name'], data: Data): JSX.Element {
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank');
openInNewTab(getLink());
} else {
history.push(getLink());
}

View File

@@ -17,6 +17,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
import { ILog } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { openInNewTab } from 'utils/navigation';
import { useContextLogData } from './useContextLogData';
@@ -116,7 +117,7 @@ function ContextLogRenderer({
);
const link = `${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`;
window.open(link, '_blank', 'noopener,noreferrer');
openInNewTab(link);
},
[query, urlQuery],
);

View File

@@ -34,6 +34,7 @@ import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { openInNewTab } from 'utils/navigation';
import { ActionItemProps } from './ActionItem';
import FieldRenderer from './FieldRenderer';
@@ -191,7 +192,7 @@ function TableView({
if (event.ctrlKey || event.metaKey) {
// open the trace in new tab
window.open(route, '_blank');
openInNewTab(route);
} else {
history.push(route);
}

View File

@@ -2,6 +2,7 @@ import { Typography } from 'antd';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history';
import { ArrowRight } from 'lucide-react';
import { openExternalLink } from 'utils/navigation';
import awwSnapUrl from '@/assets/Icons/awwSnap.svg';
@@ -14,7 +15,7 @@ export default function LogsError(): JSX.Element {
if (isCloudUserVal) {
history.push('/support');
} else {
window.open('https://signoz.io/slack', '_blank');
openExternalLink('https://signoz.io/slack');
}
};

View File

@@ -1,5 +1,6 @@
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { openInNewTab as openInNewTabFn } from 'utils/navigation';
import { TopOperationList } from './TopOperationsTable';
import { NavigateToTraceProps } from './types';
@@ -37,7 +38,7 @@ export const navigateToTrace = ({
}=${JSONCompositeQuery}`;
if (openInNewTab) {
window.open(newTraceExplorerPath, '_blank');
openInNewTabFn(newTraceExplorerPath);
} else {
safeNavigate(newTraceExplorerPath);
}

View File

@@ -9,6 +9,7 @@ import {
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { Bell, Grid } from 'lucide-react';
import { openInNewTab } from 'utils/navigation';
import { pluralize } from 'utils/pluralize';
import { DashboardsAndAlertsPopoverProps } from './types';
@@ -67,9 +68,8 @@ function DashboardsAndAlertsPopover({
<Typography.Link
key={alert.alertId}
onClick={(): void => {
window.open(
openInNewTab(
`${ROUTES.ALERT_OVERVIEW}?${QueryParams.ruleId}=${alert.alertId}`,
'_blank',
);
}}
className="dashboards-popover-content-item"
@@ -90,11 +90,8 @@ function DashboardsAndAlertsPopover({
<Typography.Link
key={dashboard.dashboardId}
onClick={(): void => {
window.open(
generatePath(ROUTES.DASHBOARD, {
dashboardId: dashboard.dashboardId,
}),
'_blank',
openInNewTab(
generatePath(ROUTES.DASHBOARD, { dashboardId: dashboard.dashboardId }),
);
}}
className="dashboards-popover-content-item"

View File

@@ -6,6 +6,7 @@ import history from 'lib/history';
import { ArrowUpRight } from 'lucide-react';
import { DataSource } from 'types/common/queryBuilder';
import DOCLINKS from 'utils/docLinks';
import { openExternalLink } from 'utils/navigation';
import eyesEmojiUrl from '@/assets/Images/eyesEmoji.svg';
@@ -42,11 +43,11 @@ export default function NoLogs({
}
history.push(link);
} else if (dataSource === 'traces') {
window.open(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE, '_blank');
openExternalLink(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE);
} else if (dataSource === DataSource.METRICS) {
window.open(DOCLINKS.METRICS_EXPLORER_EMPTY_STATE, '_blank');
openExternalLink(DOCLINKS.METRICS_EXPLORER_EMPTY_STATE);
} else {
window.open(`${DOCLINKS.USER_GUIDE}${dataSource}/`, '_blank');
openExternalLink(`${DOCLINKS.USER_GUIDE}${dataSource}/`);
}
};
return (

View File

@@ -1,3 +1,4 @@
// hippa.svg and soc2.svg do not exist in src/assets — suppressed until assets are added
import { Dot } from 'lucide-react';
import './OnboardingFooter.styles.scss';
@@ -12,7 +13,6 @@ export function OnboardingFooter(): JSX.Element {
className="footer-content"
rel="noreferrer"
>
{/* hippa.svg does not exist in src/assets — suppressed until asset is added */}
{/* eslint-disable-next-line rulesdir/no-unsupported-asset-pattern */}
<img src="/logos/hippa.svg" alt="HIPPA" className="footer-logo" />
<span className="footer-text">HIPPA</span>
@@ -24,7 +24,6 @@ export function OnboardingFooter(): JSX.Element {
className="footer-content"
rel="noreferrer"
>
{/* soc2.svg does not exist in src/assets — suppressed until asset is added */}
{/* eslint-disable-next-line rulesdir/no-unsupported-asset-pattern */}
<img src="/logos/soc2.svg" alt="SOC2" className="footer-logo" />
<span className="footer-text">SOC2</span>

View File

@@ -115,6 +115,7 @@ const useBaseAggregateOptions = ({
key={id}
icon={<LinkOutlined />}
onClick={(): void => {
// eslint-disable-next-line no-restricted-syntax -- context links can be internal or external URLs provided by users
window.open(url, '_blank');
onClose?.();
}}

View File

@@ -14,6 +14,7 @@ import { ModalTitle } from 'container/PipelinePage/PipelineListsView/styles';
import { Check, Loader, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';
import { openInNewTab } from 'utils/navigation';
import { INITIAL_ROUTING_POLICY_DETAILS_FORM_STATE } from './constants';
import {
@@ -76,7 +77,7 @@ function RoutingPolicyDetails({
style={{ padding: '0 4px' }}
type="link"
onClick={(): void => {
window.open(ROUTES.CHANNELS_NEW, '_blank');
openInNewTab(ROUTES.CHANNELS_NEW);
}}
>
here.

View File

@@ -64,7 +64,7 @@ import { USER_ROLES } from 'types/roles';
import { checkVersionState } from 'utils/app';
import { isModifierKeyPressed } from 'utils/app';
import { showErrorNotification } from 'utils/error';
import { openInNewTab } from 'utils/navigation';
import { openExternalLink, openInNewTab } from 'utils/navigation';
import signozBrandLogoUrl from '@/assets/Logos/signoz-brand-logo.svg';
@@ -818,7 +818,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
);
if (item && !('type' in item) && item.isExternal && item.url) {
window.open(item.url, '_blank');
openExternalLink(item.url);
}
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;

View File

@@ -28,6 +28,7 @@ import {
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuid } from 'uuid';
import noDataUrl from '@/assets/Icons/no-data.svg';
@@ -143,7 +144,7 @@ function SpanLogs({
const url = `${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`;
window.open(url, '_blank');
openInNewTab(url);
},
[
isLogSpanRelated,

View File

@@ -17,6 +17,7 @@ import { BarChart2, Compass, X } from 'lucide-react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Span } from 'types/api/trace/getTraceV2';
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
import { openInNewTab } from 'utils/navigation';
import { RelatedSignalsViews } from '../constants';
import SpanLogs from '../SpanLogs/SpanLogs';
@@ -157,13 +158,7 @@ function SpanRelatedSignals({
searchParams.set(QueryParams.startTime, startTimeMs.toString());
searchParams.set(QueryParams.endTime, endTimeMs.toString());
window.open(
`${window.location.origin}${
ROUTES.LOGS_EXPLORER
}?${searchParams.toString()}`,
'_blank',
'noopener,noreferrer',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${searchParams.toString()}`);
}, [selectedSpan.traceId, traceStartTime, traceEndTime]);
const emptyStateConfig = useMemo(

View File

@@ -31,6 +31,7 @@ import {
UPDATE_SPANS_AGGREGATE_PAGE_SIZE,
} from 'types/actions/trace';
import { TraceReducer } from 'types/reducer/trace';
import { openInNewTab } from 'utils/navigation';
import { v4 } from 'uuid';
dayjs.extend(duration);
@@ -214,7 +215,7 @@ function TraceTable(): JSX.Element {
event.preventDefault();
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
window.open(getLink(record), '_blank');
openInNewTab(getLink(record));
} else {
history.push(getLink(record));
}

View File

@@ -31,6 +31,7 @@ import {
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { Span } from 'types/api/trace/getTraceV2';
import { openExternalLink } from 'utils/navigation';
import { toFixed } from 'utils/toFixed';
import funnelAddUrl from '@/assets/Icons/funnel-add.svg';
@@ -547,12 +548,11 @@ function Success(props: ISuccessProps): JSX.Element {
icon={<ArrowUpRight size={14} />}
className="right-info"
type="text"
onClick={(): WindowProxy | null =>
window.open(
onClick={(): void => {
openExternalLink(
'https://signoz.io/docs/userguide/traces/#missing-spans',
'_blank',
)
}
);
}}
>
Learn More
</Button>

View File

@@ -28,6 +28,7 @@ import { useTimezone } from 'providers/Timezone';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { openInNewTab } from 'utils/navigation';
import './TracesTableComponent.styles.scss';
@@ -86,7 +87,7 @@ function TracesTableComponent({
event.preventDefault();
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
window.open(getTraceLink(record), '_blank');
openInNewTab(getTraceLink(record));
} else {
history.push(getTraceLink(record));
}

View File

@@ -17,6 +17,7 @@ import {
ConnectionUrlResponse,
GenerateConnectionUrlPayload,
} from 'types/api/integrations/aws';
import { openExternalLink } from 'utils/navigation';
import { regions } from 'utils/regions';
import logEvent from '../../../api/common/logEvent';
@@ -120,7 +121,7 @@ export function useIntegrationModal({
logEvent('AWS Integration: Account connection attempt redirected to AWS', {
id: data.account_id,
});
window.open(data.connection_url, '_blank');
openExternalLink(data.connection_url);
setModalState(ModalStateEnum.WAITING);
setAccountId(data.account_id);
},

View File

@@ -22,6 +22,7 @@ import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getGraphType } from 'utils/getGraphType';
import { openInNewTab } from 'utils/navigation';
const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
const queryRangeMutation = useMutation(getSubstituteVars);
@@ -92,7 +93,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
const url = `${ROUTES.ALERTS_NEW}?${params.toString()}`;
window.open(url, '_blank', 'noreferrer');
openInNewTab(url);
},
onError: () => {
notifications.error({

View File

@@ -0,0 +1,90 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom-v5-compat';
import { renderHook } from '@testing-library/react';
jest.mock('lib/history', () => ({
__esModule: true,
default: {
createHref: jest.fn(
({
pathname,
search,
hash,
}: {
pathname: string;
search?: string;
hash?: string;
}) => `/signoz${pathname}${search || ''}${hash || ''}`,
),
},
}));
import history from 'lib/history';
import { useSafeNavigate } from './useSafeNavigate';
function renderSafeNavigate(): ReturnType<typeof useSafeNavigate> {
const { result } = renderHook(() => useSafeNavigate(), {
wrapper: ({ children }: { children: React.ReactNode }) =>
React.createElement(MemoryRouter, { initialEntries: ['/home'] }, children),
});
return result.current;
}
describe('useSafeNavigate — newTab option', () => {
let windowOpenSpy: jest.SpyInstance;
beforeEach(() => {
windowOpenSpy = jest.spyOn(window, 'open').mockImplementation(jest.fn());
jest.clearAllMocks();
});
afterEach(() => {
windowOpenSpy.mockRestore();
});
it('opens string path in new tab with base path prepended', () => {
const { safeNavigate } = renderSafeNavigate();
safeNavigate('/traces', { newTab: true });
expect(windowOpenSpy).toHaveBeenCalledWith('/signoz/traces', '_blank');
expect(history.createHref).toHaveBeenCalledWith({
pathname: '/traces',
search: '',
hash: '',
});
});
it('preserves query string when opening in new tab', () => {
const { safeNavigate } = renderSafeNavigate();
safeNavigate('/traces?service=api', { newTab: true });
expect(windowOpenSpy).toHaveBeenCalledWith(
'/signoz/traces?service=api',
'_blank',
);
expect(history.createHref).toHaveBeenCalledWith({
pathname: '/traces',
search: '?service=api',
hash: '',
});
});
it('opens object-form path in new tab with base path prepended', () => {
const { safeNavigate } = renderSafeNavigate();
safeNavigate({ pathname: '/logs', search: '?env=prod' }, { newTab: true });
expect(windowOpenSpy).toHaveBeenCalledWith('/signoz/logs?env=prod', '_blank');
expect(history.createHref).toHaveBeenCalledWith({
pathname: '/logs',
search: '?env=prod',
});
});
it('passes external URL directly to window.open without createHref', () => {
const { safeNavigate } = renderSafeNavigate();
safeNavigate('https://docs.signoz.io/page', { newTab: true });
expect(windowOpenSpy).toHaveBeenCalledWith(
'https://docs.signoz.io/page',
'_blank',
);
expect(history.createHref).not.toHaveBeenCalled();
});
});

View File

@@ -1,5 +1,6 @@
import { useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
import history from 'lib/history';
import { cloneDeep, isEqual } from 'lodash-es';
interface NavigateOptions {
@@ -126,11 +127,30 @@ export const useSafeNavigate = (
const shouldOpenInNewTab = options?.newTab;
if (shouldOpenInNewTab) {
const targetPath =
typeof to === 'string'
? to
: `${to.pathname || location.pathname}${to.search || ''}`;
window.open(targetPath, '_blank');
if (!to) {
return;
}
let href: string;
if (typeof to === 'string') {
// 'to' may include a query string; parse before passing to createHref
// so search params are not embedded in pathname.
const parsed = new URL(to, window.location.origin);
if (parsed.origin !== window.location.origin) {
window.open(to, '_blank');
return;
}
href = history.createHref({
pathname: parsed.pathname,
search: parsed.search,
hash: parsed.hash,
});
} else {
href = history.createHref({
pathname: to.pathname ?? location.pathname,
search: to.search ?? '',
});
}
window.open(href, '_blank');
return;
}

View File

@@ -0,0 +1,39 @@
import { getBasePath, readBasePath } from './basePath';
describe('readBasePath', () => {
afterEach(() => {
document.querySelectorAll('base').forEach((el) => el.remove());
});
it('returns "/" when no <base> tag is present', () => {
expect(readBasePath()).toBe('/');
});
it('returns the href when <base href="/signoz/"> exists', () => {
const base = document.createElement('base');
base.setAttribute('href', '/signoz/');
document.head.prepend(base);
expect(readBasePath()).toBe('/signoz/');
});
it('returns "/" when <base> tag exists but has no href attribute', () => {
const base = document.createElement('base');
document.head.prepend(base);
expect(readBasePath()).toBe('/');
});
it('returns "/" when <base href="/"> exists (root deployment)', () => {
const base = document.createElement('base');
base.setAttribute('href', '/');
document.head.prepend(base);
expect(readBasePath()).toBe('/');
});
});
describe('getBasePath (module-init snapshot)', () => {
it('returns the value captured when the module was first loaded', () => {
// In Jest, no <base> tag is present when the module loads, so the
// snapshot is '/'. This test documents the singleton contract.
expect(getBasePath()).toBe('/');
});
});

View File

@@ -0,0 +1,21 @@
/**
* Reads the <base href> injected by the backend at serve time (or by
* createHtmlPlugin in dev). Returns '/' if no <base> tag is present,
* which matches root-path deployments with no backend injection.
*
* Called once at module init — result is stable for the page lifetime.
* Exported for testing; consumers should use getBasePath().
*/
export function readBasePath(): string {
if (typeof document === 'undefined') {
return '/';
}
return document.querySelector('base')?.getAttribute('href') ?? '/';
}
/** @internal Use getBasePath() in application code. */
export const basePath: string = readBasePath();
export function getBasePath(): string {
return basePath;
}

View File

@@ -1,3 +1,9 @@
import { createBrowserHistory } from 'history';
export default createBrowserHistory();
import { getBasePath } from './basePath';
// Strip the trailing slash that <base href> includes ('/signoz/' → '/signoz')
// because createBrowserHistory expects a basename without trailing slash.
const basename = getBasePath().replace(/\/$/, '') || undefined;
export default createBrowserHistory({ basename });

View File

@@ -2,6 +2,7 @@ import { useCallback } from 'react';
import { Button } from 'antd';
import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history';
import { Home, LifeBuoy } from 'lucide-react';
import { handleContactSupport } from 'pages/Integrations/utils';
@@ -11,8 +12,7 @@ import './ErrorBoundaryFallback.styles.scss';
function ErrorBoundaryFallback(): JSX.Element {
const handleReload = (): void => {
// Go to home page
window.location.href = ROUTES.HOME;
history.push(ROUTES.HOME);
};
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();

View File

@@ -1,10 +1,11 @@
import history from 'lib/history';
import { openExternalLink } from 'utils/navigation';
export const handleContactSupport = (isCloudUser: boolean): void => {
if (isCloudUser) {
history.push('/support');
} else {
window.open('https://signoz.io/slack', '_blank');
openExternalLink('https://signoz.io/slack');
}
};

View File

@@ -19,6 +19,7 @@ import {
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import {
convertToMilliseconds,
@@ -93,7 +94,7 @@ export function getColumns(
key={item}
className="traceid-text"
onClick={(): void => {
window.open(`${ROUTES.TRACE}/${item}`, '_blank');
openInNewTab(`${ROUTES.TRACE}/${item}`);
logEvent(`MQ Kafka: Drop Rate - traceid navigation`, {
item,
});
@@ -123,7 +124,7 @@ export function getColumns(
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
window.open(`/services/${encodeURIComponent(text)}`, '_blank');
openInNewTab(`/services/${encodeURIComponent(text)}`);
}}
>
{text}

View File

@@ -10,7 +10,7 @@ import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import { openExternalLink, openInNewTab } from 'utils/navigation';
import {
KAFKA_SETUP_DOC_LINK,
@@ -59,7 +59,7 @@ function MessagingQueues(): JSX.Element {
history.push(link);
}
} else {
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
openExternalLink(KAFKA_SETUP_DOC_LINK);
}
};

View File

@@ -20,6 +20,7 @@ import { useAppContext } from 'providers/App/App';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import APIError from 'types/api/error';
import { openExternalLink } from 'utils/navigation';
import './Support.styles.scss';
@@ -92,7 +93,7 @@ export default function Support(): JSX.Element {
const { pathname } = useLocation();
const handleChannelWithRedirects = (url: string): void => {
window.open(url, '_blank');
openExternalLink(url);
};
useEffect(() => {

View File

@@ -1,80 +1,140 @@
import { isModifierKeyPressed } from '../app';
import { openInNewTab } from '../navigation';
import { openExternalLink, openInNewTab } from '../navigation';
describe('navigation utilities', () => {
const originalWindowOpen = window.open;
beforeEach(() => {
window.open = jest.fn();
});
afterEach(() => {
window.open = originalWindowOpen;
});
describe('isModifierKeyPressed', () => {
const createMouseEvent = (overrides: Partial<MouseEvent> = {}): MouseEvent =>
// Mock history before importing navigation so history.createHref is controlled.
jest.mock('lib/history', () => ({
__esModule: true,
default: {
createHref: jest.fn(
({
metaKey: false,
ctrlKey: false,
button: 0,
...overrides,
} as MouseEvent);
pathname,
search,
hash,
}: {
pathname: string;
search?: string;
hash?: string;
}) => `/signoz${pathname}${search || ''}${hash || ''}`,
),
},
}));
it('returns true when metaKey is pressed (Cmd on Mac)', () => {
const event = createMouseEvent({ metaKey: true });
expect(isModifierKeyPressed(event)).toBe(true);
});
const mockWindowOpen = jest.fn();
Object.defineProperty(window, 'open', {
value: mockWindowOpen,
writable: true,
});
it('returns true when ctrlKey is pressed (Ctrl on Windows/Linux)', () => {
const event = createMouseEvent({ ctrlKey: true });
expect(isModifierKeyPressed(event)).toBe(true);
});
describe('openInNewTab', () => {
beforeEach(() => mockWindowOpen.mockClear());
it('returns true when both metaKey and ctrlKey are pressed', () => {
const event = createMouseEvent({ metaKey: true, ctrlKey: true });
expect(isModifierKeyPressed(event)).toBe(true);
});
// Previously: window.open(path, '_blank') — no base path prepended.
// Now: internal paths go through history.createHref so sub-path
// deployments (e.g. /signoz/) are handled correctly.
it('returns false when neither modifier key is pressed', () => {
const event = createMouseEvent();
expect(isModifierKeyPressed(event)).toBe(false);
});
it('returns false when only shiftKey or altKey are pressed', () => {
const event = createMouseEvent({
shiftKey: true,
altKey: true,
} as Partial<MouseEvent>);
expect(isModifierKeyPressed(event)).toBe(false);
});
it('returns true when middle mouse button is used', () => {
const event = createMouseEvent({ button: 1 });
expect(isModifierKeyPressed(event)).toBe(true);
});
it('prepends base path to internal path via history.createHref', () => {
openInNewTab('/dashboard');
expect(mockWindowOpen).toHaveBeenCalledWith('/signoz/dashboard', '_blank');
});
describe('openInNewTab', () => {
it('calls window.open with the given path and _blank target', () => {
openInNewTab('/dashboard');
expect(window.open).toHaveBeenCalledWith('/dashboard', '_blank');
});
it('preserves query string when prepending base path', () => {
openInNewTab('/alerts?tab=AlertRules&relativeTime=30m');
expect(mockWindowOpen).toHaveBeenCalledWith(
'/signoz/alerts?tab=AlertRules&relativeTime=30m',
'_blank',
);
});
it('handles full URLs', () => {
openInNewTab('https://example.com/page');
expect(window.open).toHaveBeenCalledWith(
'https://example.com/page',
'_blank',
);
});
it('preserves hash when prepending base path', () => {
openInNewTab('/dashboard/123#panel-5');
expect(mockWindowOpen).toHaveBeenCalledWith(
'/signoz/dashboard/123#panel-5',
'_blank',
);
});
it('handles paths with query strings', () => {
openInNewTab('/alerts?tab=AlertRules&relativeTime=30m');
expect(window.open).toHaveBeenCalledWith(
'/alerts?tab=AlertRules&relativeTime=30m',
'_blank',
);
});
// External URLs bypass createHref and are passed through as-is.
it('passes http URL directly to window.open without base path', () => {
openInNewTab('https://example.com/page');
expect(mockWindowOpen).toHaveBeenCalledWith(
'https://example.com/page',
'_blank',
);
});
it('passes protocol-relative URL directly to window.open without base path', () => {
openInNewTab('//cdn.example.com/asset.js');
expect(mockWindowOpen).toHaveBeenCalledWith(
'//cdn.example.com/asset.js',
'_blank',
);
});
});
describe('isModifierKeyPressed', () => {
const createMouseEvent = (overrides: Partial<MouseEvent> = {}): MouseEvent =>
({
metaKey: false,
ctrlKey: false,
button: 0,
...overrides,
} as MouseEvent);
it('returns true when metaKey is pressed (Cmd on Mac)', () => {
const event = createMouseEvent({ metaKey: true });
expect(isModifierKeyPressed(event)).toBe(true);
});
it('returns true when ctrlKey is pressed (Ctrl on Windows/Linux)', () => {
const event = createMouseEvent({ ctrlKey: true });
expect(isModifierKeyPressed(event)).toBe(true);
});
it('returns true when both metaKey and ctrlKey are pressed', () => {
const event = createMouseEvent({ metaKey: true, ctrlKey: true });
expect(isModifierKeyPressed(event)).toBe(true);
});
it('returns false when neither modifier key is pressed', () => {
const event = createMouseEvent();
expect(isModifierKeyPressed(event)).toBe(false);
});
it('returns false when only shiftKey or altKey are pressed', () => {
const event = createMouseEvent({
shiftKey: true,
altKey: true,
} as Partial<MouseEvent>);
expect(isModifierKeyPressed(event)).toBe(false);
});
it('returns true when middle mouse button is used', () => {
const event = createMouseEvent({ button: 1 });
expect(isModifierKeyPressed(event)).toBe(true);
});
});
describe('openExternalLink', () => {
beforeEach(() => mockWindowOpen.mockClear());
// openExternalLink is new — replaces ad-hoc window.open calls for external
// URLs and always adds noopener,noreferrer for security.
it('opens external URL with noopener,noreferrer', () => {
openExternalLink('https://signoz.io/slack');
expect(mockWindowOpen).toHaveBeenCalledWith(
'https://signoz.io/slack',
'_blank',
'noopener,noreferrer',
);
});
it('opens protocol-relative external URL with noopener,noreferrer', () => {
openExternalLink('//docs.signoz.io/setup');
expect(mockWindowOpen).toHaveBeenCalledWith(
'//docs.signoz.io/setup',
'_blank',
'noopener,noreferrer',
);
});
});

View File

@@ -0,0 +1,95 @@
// Mock history before importing navigation, so history.createHref is controlled.
jest.mock('lib/history', () => ({
__esModule: true,
default: {
createHref: jest.fn(
({
pathname,
search,
hash,
}: {
pathname: string;
search?: string;
hash?: string;
}) => `/signoz${pathname}${search || ''}${hash || ''}`,
),
},
}));
import { openExternalLink, openInNewTab } from './navigation';
const mockWindowOpen = jest.fn();
Object.defineProperty(window, 'open', {
value: mockWindowOpen,
writable: true,
});
describe('openInNewTab', () => {
beforeEach(() => mockWindowOpen.mockClear());
it('opens external http URL as-is without prepending base path', () => {
openInNewTab('https://signoz.io/docs');
expect(mockWindowOpen).toHaveBeenCalledWith(
'https://signoz.io/docs',
'_blank',
);
});
it('opens external protocol-relative URL as-is', () => {
openInNewTab('//cdn.example.com/asset.js');
expect(mockWindowOpen).toHaveBeenCalledWith(
'//cdn.example.com/asset.js',
'_blank',
);
});
it('prepends base path to internal path via history.createHref', () => {
openInNewTab('/traces');
expect(mockWindowOpen).toHaveBeenCalledWith('/signoz/traces', '_blank');
});
it('preserves query string when prepending base path', () => {
openInNewTab('/traces?service=frontend&env=prod');
expect(mockWindowOpen).toHaveBeenCalledWith(
'/signoz/traces?service=frontend&env=prod',
'_blank',
);
});
it('preserves hash when prepending base path', () => {
openInNewTab('/dashboard/123#panel-5');
expect(mockWindowOpen).toHaveBeenCalledWith(
'/signoz/dashboard/123#panel-5',
'_blank',
);
});
it('handles path without leading slash by resolving against origin', () => {
openInNewTab('traces');
// new URL('traces', window.location.origin) resolves to origin + '/traces'
// history.createHref is called with pathname: '/traces'
expect(mockWindowOpen).toHaveBeenCalledWith('/signoz/traces', '_blank');
});
});
describe('openExternalLink', () => {
beforeEach(() => mockWindowOpen.mockClear());
it('opens external URL with noopener,noreferrer', () => {
openExternalLink('https://signoz.io/slack');
expect(mockWindowOpen).toHaveBeenCalledWith(
'https://signoz.io/slack',
'_blank',
'noopener,noreferrer',
);
});
it('opens protocol-relative external URL', () => {
openExternalLink('//docs.signoz.io/setup');
expect(mockWindowOpen).toHaveBeenCalledWith(
'//docs.signoz.io/setup',
'_blank',
'noopener,noreferrer',
);
});
});

View File

@@ -1,6 +1,37 @@
import history from 'lib/history';
/**
* Opens the given path in a new browser tab.
* Opens an internal SigNoz path in a new tab.
* Automatically prepends the runtime base path via history.createHref so
* sub-path deployments (e.g. /signoz/) work correctly.
*
* For external URLs (http/https), use openExternalLink() instead.
*/
export const openInNewTab = (path: string): void => {
window.open(path, '_blank');
if (path.startsWith('http') || path.startsWith('//')) {
window.open(path, '_blank');
return;
}
// Parse the path so query params and hash are passed to createHref
// separately — passing a full URL string as `pathname` embeds the search
// string inside the path segment, which is incorrect.
const parsed = new URL(path, window.location.origin);
window.open(
history.createHref({
pathname: parsed.pathname,
search: parsed.search,
hash: parsed.hash,
}),
'_blank',
);
};
/**
* Opens an external URL in a new tab with noopener,noreferrer.
* Use this for links to external sites (docs, Slack, marketing pages).
*
* For internal SigNoz routes, use openInNewTab() instead.
*/
export const openExternalLink = (url: string): void => {
window.open(url, '_blank', 'noopener,noreferrer');
};

View File

@@ -38,6 +38,7 @@ export default defineConfig(
data: {
PYLON_APP_ID: env.VITE_PYLON_APP_ID || '',
APPCUES_APP_ID: env.VITE_APPCUES_APP_ID || '',
BASE_PATH: env.VITE_BASE_PATH || '/', // ← REMOVE when BE injection is live
},
},
}),
@@ -81,6 +82,7 @@ export default defineConfig(
return {
plugins,
base: './',
resolve: {
alias: {
'@': resolve(__dirname, './src'),

View File

@@ -1,4 +1,4 @@
package signozalertmanager
package alertmanager
import (
"context"
@@ -7,7 +7,6 @@ import (
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
@@ -16,15 +15,17 @@ import (
"github.com/gorilla/mux"
)
type handler struct {
alertmanager alertmanager.Alertmanager
type API struct {
alertmanager Alertmanager
}
func NewHandler(alertmanager alertmanager.Alertmanager) alertmanager.Handler {
return &handler{alertmanager: alertmanager}
func NewAPI(alertmanager Alertmanager) *API {
return &API{
alertmanager: alertmanager,
}
}
func (handler *handler) GetAlerts(rw http.ResponseWriter, req *http.Request) {
func (api *API) GetAlerts(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -40,7 +41,7 @@ func (handler *handler) GetAlerts(rw http.ResponseWriter, req *http.Request) {
return
}
alerts, err := handler.alertmanager.GetAlerts(ctx, claims.OrgID, params)
alerts, err := api.alertmanager.GetAlerts(ctx, claims.OrgID, params)
if err != nil {
render.Error(rw, err)
return
@@ -49,7 +50,7 @@ func (handler *handler) GetAlerts(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, alerts)
}
func (handler *handler) TestReceiver(rw http.ResponseWriter, req *http.Request) {
func (api *API) TestReceiver(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -72,7 +73,7 @@ func (handler *handler) TestReceiver(rw http.ResponseWriter, req *http.Request)
return
}
err = handler.alertmanager.TestReceiver(ctx, claims.OrgID, receiver)
err = api.alertmanager.TestReceiver(ctx, claims.OrgID, receiver)
if err != nil {
render.Error(rw, err)
return
@@ -81,7 +82,7 @@ func (handler *handler) TestReceiver(rw http.ResponseWriter, req *http.Request)
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) ListChannels(rw http.ResponseWriter, req *http.Request) {
func (api *API) ListChannels(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -91,7 +92,7 @@ func (handler *handler) ListChannels(rw http.ResponseWriter, req *http.Request)
return
}
channels, err := handler.alertmanager.ListChannels(ctx, claims.OrgID)
channels, err := api.alertmanager.ListChannels(ctx, claims.OrgID)
if err != nil {
render.Error(rw, err)
return
@@ -105,11 +106,11 @@ func (handler *handler) ListChannels(rw http.ResponseWriter, req *http.Request)
render.Success(rw, http.StatusOK, channels)
}
func (handler *handler) ListAllChannels(rw http.ResponseWriter, req *http.Request) {
func (api *API) ListAllChannels(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
channels, err := handler.alertmanager.ListAllChannels(ctx)
channels, err := api.alertmanager.ListAllChannels(ctx)
if err != nil {
render.Error(rw, err)
return
@@ -118,7 +119,7 @@ func (handler *handler) ListAllChannels(rw http.ResponseWriter, req *http.Reques
render.Success(rw, http.StatusOK, channels)
}
func (handler *handler) GetChannelByID(rw http.ResponseWriter, req *http.Request) {
func (api *API) GetChannelByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -146,7 +147,7 @@ func (handler *handler) GetChannelByID(rw http.ResponseWriter, req *http.Request
return
}
channel, err := handler.alertmanager.GetChannelByID(ctx, claims.OrgID, id)
channel, err := api.alertmanager.GetChannelByID(ctx, claims.OrgID, id)
if err != nil {
render.Error(rw, err)
return
@@ -155,7 +156,7 @@ func (handler *handler) GetChannelByID(rw http.ResponseWriter, req *http.Request
render.Success(rw, http.StatusOK, channel)
}
func (handler *handler) UpdateChannelByID(rw http.ResponseWriter, req *http.Request) {
func (api *API) UpdateChannelByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -196,7 +197,7 @@ func (handler *handler) UpdateChannelByID(rw http.ResponseWriter, req *http.Requ
return
}
err = handler.alertmanager.UpdateChannelByReceiverAndID(ctx, claims.OrgID, receiver, id)
err = api.alertmanager.UpdateChannelByReceiverAndID(ctx, claims.OrgID, receiver, id)
if err != nil {
render.Error(rw, err)
return
@@ -205,7 +206,7 @@ func (handler *handler) UpdateChannelByID(rw http.ResponseWriter, req *http.Requ
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) DeleteChannelByID(rw http.ResponseWriter, req *http.Request) {
func (api *API) DeleteChannelByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -233,7 +234,7 @@ func (handler *handler) DeleteChannelByID(rw http.ResponseWriter, req *http.Requ
return
}
err = handler.alertmanager.DeleteChannelByID(ctx, claims.OrgID, id)
err = api.alertmanager.DeleteChannelByID(ctx, claims.OrgID, id)
if err != nil {
render.Error(rw, err)
return
@@ -242,7 +243,7 @@ func (handler *handler) DeleteChannelByID(rw http.ResponseWriter, req *http.Requ
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) CreateChannel(rw http.ResponseWriter, req *http.Request) {
func (api *API) CreateChannel(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -265,7 +266,7 @@ func (handler *handler) CreateChannel(rw http.ResponseWriter, req *http.Request)
return
}
channel, err := handler.alertmanager.CreateChannel(ctx, claims.OrgID, receiver)
channel, err := api.alertmanager.CreateChannel(ctx, claims.OrgID, receiver)
if err != nil {
render.Error(rw, err)
return
@@ -274,7 +275,7 @@ func (handler *handler) CreateChannel(rw http.ResponseWriter, req *http.Request)
render.Success(rw, http.StatusCreated, channel)
}
func (handler *handler) CreateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
func (api *API) CreateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -299,7 +300,7 @@ func (handler *handler) CreateRoutePolicy(rw http.ResponseWriter, req *http.Requ
return
}
result, err := handler.alertmanager.CreateRoutePolicy(ctx, &policy)
result, err := api.alertmanager.CreateRoutePolicy(ctx, &policy)
if err != nil {
render.Error(rw, err)
return
@@ -308,11 +309,11 @@ func (handler *handler) CreateRoutePolicy(rw http.ResponseWriter, req *http.Requ
render.Success(rw, http.StatusCreated, result)
}
func (handler *handler) GetAllRoutePolicies(rw http.ResponseWriter, req *http.Request) {
func (api *API) GetAllRoutePolicies(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
policies, err := handler.alertmanager.GetAllRoutePolicies(ctx)
policies, err := api.alertmanager.GetAllRoutePolicies(ctx)
if err != nil {
render.Error(rw, err)
return
@@ -321,7 +322,7 @@ func (handler *handler) GetAllRoutePolicies(rw http.ResponseWriter, req *http.Re
render.Success(rw, http.StatusOK, policies)
}
func (handler *handler) GetRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
func (api *API) GetRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -332,7 +333,7 @@ func (handler *handler) GetRoutePolicyByID(rw http.ResponseWriter, req *http.Req
return
}
policy, err := handler.alertmanager.GetRoutePolicyByID(ctx, policyID)
policy, err := api.alertmanager.GetRoutePolicyByID(ctx, policyID)
if err != nil {
render.Error(rw, err)
return
@@ -341,7 +342,7 @@ func (handler *handler) GetRoutePolicyByID(rw http.ResponseWriter, req *http.Req
render.Success(rw, http.StatusOK, policy)
}
func (handler *handler) DeleteRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
func (api *API) DeleteRoutePolicyByID(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -352,7 +353,7 @@ func (handler *handler) DeleteRoutePolicyByID(rw http.ResponseWriter, req *http.
return
}
err := handler.alertmanager.DeleteRoutePolicyByID(ctx, policyID)
err := api.alertmanager.DeleteRoutePolicyByID(ctx, policyID)
if err != nil {
render.Error(rw, err)
return
@@ -361,7 +362,7 @@ func (handler *handler) DeleteRoutePolicyByID(rw http.ResponseWriter, req *http.
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) UpdateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
func (api *API) UpdateRoutePolicy(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
@@ -391,7 +392,7 @@ func (handler *handler) UpdateRoutePolicy(rw http.ResponseWriter, req *http.Requ
return
}
result, err := handler.alertmanager.UpdateRoutePolicyByID(ctx, policyID, &policy)
result, err := api.alertmanager.UpdateRoutePolicyByID(ctx, policyID, &policy)
if err != nil {
render.Error(rw, err)
return

View File

@@ -1,31 +0,0 @@
package alertmanager
import "net/http"
type Handler interface {
GetAlerts(http.ResponseWriter, *http.Request)
TestReceiver(http.ResponseWriter, *http.Request)
ListChannels(http.ResponseWriter, *http.Request)
ListAllChannels(http.ResponseWriter, *http.Request)
GetChannelByID(http.ResponseWriter, *http.Request)
CreateChannel(http.ResponseWriter, *http.Request)
UpdateChannelByID(http.ResponseWriter, *http.Request)
DeleteChannelByID(http.ResponseWriter, *http.Request)
GetAllRoutePolicies(http.ResponseWriter, *http.Request)
GetRoutePolicyByID(http.ResponseWriter, *http.Request)
CreateRoutePolicy(http.ResponseWriter, *http.Request)
UpdateRoutePolicy(http.ResponseWriter, *http.Request)
DeleteRoutePolicyByID(http.ResponseWriter, *http.Request)
}

View File

@@ -223,8 +223,6 @@ func (r *provider) Match(ctx context.Context, orgID string, ruleID string, set m
for _, route := range expressionRoutes {
evaluateExpr, err := r.evaluateExpr(ctx, route.Expression, set)
if err != nil {
//nolint:sloglint
r.settings.Logger().WarnContext(ctx, "failed to evaluate route policy expression", errors.Attr(err), slog.String("rule.id", ruleID))
continue
}
if evaluateExpr {
@@ -300,7 +298,7 @@ func (r *provider) convertLabelSetToEnv(ctx context.Context, labelSet model.Labe
func (r *provider) evaluateExpr(ctx context.Context, expression string, labelSet model.LabelSet) (bool, error) {
env := r.convertLabelSetToEnv(ctx, labelSet)
program, err := expr.Compile(expression, expr.Env(env), expr.AllowUndefinedVariables())
program, err := expr.Compile(expression, expr.Env(env))
if err != nil {
return false, errors.NewInternalf(errors.CodeInternal, "error compiling route policy %s: %v", expression, err)
}

View File

@@ -644,22 +644,6 @@ func TestProvider_EvaluateExpression(t *testing.T) {
},
expected: true,
},
{
name: "nonexistent key OR check",
expression: `threshold.name = 'warning' OR ruleId = 'rule1'`,
labelSet: model.LabelSet{
"threshold.name": "warning",
},
expected: true,
},
{
name: "nonexistent key && check",
expression: `threshold.name = 'warning' && nonexistent = 'auth'`,
labelSet: model.LabelSet{
"threshold.name": "warning",
},
expected: false,
},
}
for _, tt := range tests {

View File

@@ -309,8 +309,8 @@ func (provider *provider) CreateRoutePolicy(ctx context.Context, routeRequest *a
return &alertmanagertypes.GettableRoutePolicy{
PostableRoutePolicy: *routeRequest,
ID: route.ID.StringValue(),
CreatedAt: route.CreatedAt,
UpdatedAt: route.UpdatedAt,
CreatedAt: &route.CreatedAt,
UpdatedAt: &route.UpdatedAt,
CreatedBy: &route.CreatedBy,
UpdatedBy: &route.UpdatedBy,
}, nil
@@ -365,8 +365,8 @@ func (provider *provider) CreateRoutePolicies(ctx context.Context, routeRequests
results = append(results, &alertmanagertypes.GettableRoutePolicy{
PostableRoutePolicy: *routeRequest,
ID: route.ID.StringValue(),
CreatedAt: route.CreatedAt,
UpdatedAt: route.UpdatedAt,
CreatedAt: &route.CreatedAt,
UpdatedAt: &route.UpdatedAt,
CreatedBy: &route.CreatedBy,
UpdatedBy: &route.UpdatedBy,
})
@@ -405,8 +405,8 @@ func (provider *provider) GetRoutePolicyByID(ctx context.Context, routeID string
Tags: route.Tags,
},
ID: route.ID.StringValue(),
CreatedAt: route.CreatedAt,
UpdatedAt: route.UpdatedAt,
CreatedAt: &route.CreatedAt,
UpdatedAt: &route.UpdatedAt,
CreatedBy: &route.CreatedBy,
UpdatedBy: &route.UpdatedBy,
}, nil
@@ -439,8 +439,8 @@ func (provider *provider) GetAllRoutePolicies(ctx context.Context) ([]*alertmana
Tags: route.Tags,
},
ID: route.ID.StringValue(),
CreatedAt: route.CreatedAt,
UpdatedAt: route.UpdatedAt,
CreatedAt: &route.CreatedAt,
UpdatedAt: &route.UpdatedAt,
CreatedBy: &route.CreatedBy,
UpdatedBy: &route.UpdatedBy,
})
@@ -508,8 +508,8 @@ func (provider *provider) UpdateRoutePolicyByID(ctx context.Context, routeID str
return &alertmanagertypes.GettableRoutePolicy{
PostableRoutePolicy: *route,
ID: updatedRoute.ID.StringValue(),
CreatedAt: updatedRoute.CreatedAt,
UpdatedAt: updatedRoute.UpdatedAt,
CreatedAt: &updatedRoute.CreatedAt,
UpdatedAt: &updatedRoute.UpdatedAt,
CreatedBy: &updatedRoute.CreatedBy,
UpdatedBy: &updatedRoute.UpdatedBy,
}, nil

View File

@@ -1,235 +0,0 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/gorilla/mux"
)
func (provider *provider) addAlertmanagerRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/channels", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.ListChannels), handler.OpenAPIDef{
ID: "ListChannels",
Tags: []string{"channels"},
Summary: "List notification channels",
Description: "This endpoint lists all notification channels for the organization",
Request: nil,
RequestContentType: "",
Response: make([]*alertmanagertypes.Channel, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.GetChannelByID), handler.OpenAPIDef{
ID: "GetChannelByID",
Tags: []string{"channels"},
Summary: "Get notification channel by ID",
Description: "This endpoint returns a notification channel by ID",
Request: nil,
RequestContentType: "",
Response: new(alertmanagertypes.Channel),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/channels", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.CreateChannel), handler.OpenAPIDef{
ID: "CreateChannel",
Tags: []string{"channels"},
Summary: "Create notification channel",
Description: "This endpoint creates a notification channel",
Request: new(alertmanagertypes.Receiver),
RequestContentType: "application/json",
Response: new(alertmanagertypes.Channel),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.UpdateChannelByID), handler.OpenAPIDef{
ID: "UpdateChannelByID",
Tags: []string{"channels"},
Summary: "Update notification channel",
Description: "This endpoint updates a notification channel by ID",
Request: new(alertmanagertypes.Receiver),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/channels/{id}", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.DeleteChannelByID), handler.OpenAPIDef{
ID: "DeleteChannelByID",
Tags: []string{"channels"},
Summary: "Delete notification channel",
Description: "This endpoint deletes a notification channel by ID",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/channels/test", handler.New(provider.authZ.EditAccess(provider.alertmanagerHandler.TestReceiver), handler.OpenAPIDef{
ID: "TestChannel",
Tags: []string{"channels"},
Summary: "Test notification channel",
Description: "This endpoint tests a notification channel by sending a test notification",
Request: new(alertmanagertypes.Receiver),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/testChannel", handler.New(provider.authZ.EditAccess(provider.alertmanagerHandler.TestReceiver), handler.OpenAPIDef{
ID: "TestChannelDeprecated",
Tags: []string{"channels"},
Summary: "Test notification channel (deprecated)",
Description: "Deprecated: use /api/v1/channels/test instead",
Request: new(alertmanagertypes.Receiver),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: true,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/route_policies", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.GetAllRoutePolicies), handler.OpenAPIDef{
ID: "GetAllRoutePolicies",
Tags: []string{"routepolicies"},
Summary: "List route policies",
Description: "This endpoint lists all route policies for the organization",
Request: nil,
RequestContentType: "",
Response: make([]*alertmanagertypes.GettableRoutePolicy, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.GetRoutePolicyByID), handler.OpenAPIDef{
ID: "GetRoutePolicyByID",
Tags: []string{"routepolicies"},
Summary: "Get route policy by ID",
Description: "This endpoint returns a route policy by ID",
Request: nil,
RequestContentType: "",
Response: new(alertmanagertypes.GettableRoutePolicy),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/route_policies", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.CreateRoutePolicy), handler.OpenAPIDef{
ID: "CreateRoutePolicy",
Tags: []string{"routepolicies"},
Summary: "Create route policy",
Description: "This endpoint creates a route policy",
Request: new(alertmanagertypes.PostableRoutePolicy),
RequestContentType: "application/json",
Response: new(alertmanagertypes.GettableRoutePolicy),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.UpdateRoutePolicy), handler.OpenAPIDef{
ID: "UpdateRoutePolicy",
Tags: []string{"routepolicies"},
Summary: "Update route policy",
Description: "This endpoint updates a route policy by ID",
Request: new(alertmanagertypes.PostableRoutePolicy),
RequestContentType: "application/json",
Response: new(alertmanagertypes.GettableRoutePolicy),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/route_policies/{id}", handler.New(provider.authZ.AdminAccess(provider.alertmanagerHandler.DeleteRoutePolicyByID), handler.OpenAPIDef{
ID: "DeleteRoutePolicyByID",
Tags: []string{"routepolicies"},
Summary: "Delete route policy",
Description: "This endpoint deletes a route policy by ID",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/alerts", handler.New(provider.authZ.ViewAccess(provider.alertmanagerHandler.GetAlerts), handler.OpenAPIDef{
ID: "GetAlerts",
Tags: []string{"alerts"},
Summary: "Get alerts",
Description: "This endpoint returns alerts for the organization",
Request: nil,
RequestContentType: "",
Response: make(alertmanagertypes.DeprecatedGettableAlerts, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -3,7 +3,6 @@ package signozapiserver
import (
"context"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apiserver"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/factory"
@@ -58,7 +57,6 @@ type provider struct {
factoryHandler factory.Handler
cloudIntegrationHandler cloudintegration.Handler
ruleStateHistoryHandler rulestatehistory.Handler
alertmanagerHandler alertmanager.Handler
}
func NewFactory(
@@ -85,7 +83,6 @@ func NewFactory(
factoryHandler factory.Handler,
cloudIntegrationHandler cloudintegration.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
alertmanagerHandler alertmanager.Handler,
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
return newProvider(
@@ -115,7 +112,6 @@ func NewFactory(
factoryHandler,
cloudIntegrationHandler,
ruleStateHistoryHandler,
alertmanagerHandler,
)
})
}
@@ -147,7 +143,6 @@ func newProvider(
factoryHandler factory.Handler,
cloudIntegrationHandler cloudintegration.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
alertmanagerHandler alertmanager.Handler,
) (apiserver.APIServer, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
router := mux.NewRouter().UseEncodedPath()
@@ -177,7 +172,6 @@ func newProvider(
factoryHandler: factoryHandler,
cloudIntegrationHandler: cloudIntegrationHandler,
ruleStateHistoryHandler: ruleStateHistoryHandler,
alertmanagerHandler: alertmanagerHandler,
}
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
@@ -278,10 +272,6 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addAlertmanagerRoutes(router); err != nil {
return err
}
return nil
}

View File

@@ -22,7 +22,7 @@ func newConfig() factory.Config {
Agent: AgentConfig{
// we will maintain the latest version of cloud integration agent from here,
// till we automate it externally or figure out a way to validate it.
Version: "v0.0.9",
Version: "v0.0.8",
},
}
}

View File

@@ -27,6 +27,7 @@ import (
"github.com/prometheus/prometheus/promql"
"github.com/SigNoz/signoz/pkg/alertmanager"
errorsV2 "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/http/render"
@@ -139,6 +140,8 @@ type APIHandler struct {
pvcsRepo *inframetrics.PvcsRepo
AlertmanagerAPI *alertmanager.API
LicensingAPI licensing.API
QueryParserAPI *queryparser.API
@@ -165,6 +168,8 @@ type APIHandlerOpts struct {
// Flux Interval
FluxInterval time.Duration
AlertmanagerAPI *alertmanager.API
LicensingAPI licensing.API
QueryParserAPI *queryparser.API
@@ -225,6 +230,7 @@ func NewAPIHandler(opts APIHandlerOpts, config signoz.Config) (*APIHandler, erro
statefulsetsRepo: statefulsetsRepo,
jobsRepo: jobsRepo,
pvcsRepo: pvcsRepo,
AlertmanagerAPI: opts.AlertmanagerAPI,
LicensingAPI: opts.LicensingAPI,
Signoz: opts.Signoz,
QueryParserAPI: opts.QueryParserAPI,
@@ -496,6 +502,21 @@ func (aH *APIHandler) Respond(w http.ResponseWriter, data interface{}) {
func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/query_range", am.ViewAccess(aH.queryRangeMetrics)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/query", am.ViewAccess(aH.queryMetrics)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/channels", am.ViewAccess(aH.AlertmanagerAPI.ListChannels)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/channels/{id}", am.ViewAccess(aH.AlertmanagerAPI.GetChannelByID)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/channels/{id}", am.AdminAccess(aH.AlertmanagerAPI.UpdateChannelByID)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/channels/{id}", am.AdminAccess(aH.AlertmanagerAPI.DeleteChannelByID)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/channels", am.EditAccess(aH.AlertmanagerAPI.CreateChannel)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/testChannel", am.EditAccess(aH.AlertmanagerAPI.TestReceiver)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/route_policies", am.ViewAccess(aH.AlertmanagerAPI.GetAllRoutePolicies)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/route_policies/{id}", am.ViewAccess(aH.AlertmanagerAPI.GetRoutePolicyByID)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/route_policies", am.AdminAccess(aH.AlertmanagerAPI.CreateRoutePolicy)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/route_policies/{id}", am.AdminAccess(aH.AlertmanagerAPI.DeleteRoutePolicyByID)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/route_policies/{id}", am.AdminAccess(aH.AlertmanagerAPI.UpdateRoutePolicy)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/alerts", am.ViewAccess(aH.AlertmanagerAPI.GetAlerts)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules", am.ViewAccess(aH.listRules)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules/{id}", am.ViewAccess(aH.getRule)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/rules", am.EditAccess(aH.createRule)).Methods(http.MethodPost)

View File

@@ -136,6 +136,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
CloudIntegrationsController: cloudIntegrationsController,
LogsParsingPipelineController: logParsingPipelineController,
FluxInterval: config.Querier.FluxInterval,
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
LicensingAPI: nooplicensing.NewLicenseAPI(),
Signoz: signoz,
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),

View File

@@ -1,8 +1,6 @@
package signoz
import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/authz/signozauthzapi"
@@ -64,7 +62,6 @@ type Handlers struct {
RegistryHandler factory.Handler
CloudIntegrationHandler cloudintegration.Handler
RuleStateHistory rulestatehistory.Handler
AlertmanagerHandler alertmanager.Handler
}
func NewHandlers(
@@ -80,7 +77,6 @@ func NewHandlers(
authz authz.AuthZ,
zeusService zeus.Zeus,
registryHandler factory.Handler,
alertmanagerService alertmanager.Alertmanager,
) Handlers {
return Handlers{
SavedView: implsavedview.NewHandler(modules.SavedView),
@@ -103,6 +99,5 @@ func NewHandlers(
RegistryHandler: registryHandler,
RuleStateHistory: implrulestatehistory.NewHandler(modules.RuleStateHistory),
CloudIntegrationHandler: implcloudintegration.NewHandler(modules.CloudIntegration),
AlertmanagerHandler: signozalertmanager.NewHandler(alertmanagerService),
}
}

View File

@@ -56,7 +56,7 @@ func TestNewHandlers(t *testing.T) {
querierHandler := querier.NewHandler(providerSettings, nil, nil)
registryHandler := factory.NewHandler(nil)
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil, registryHandler, alertmanager)
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil, registryHandler)
reflectVal := reflect.ValueOf(handlers)
for i := 0; i < reflectVal.NumField(); i++ {
f := reflectVal.Field(i)

View File

@@ -7,7 +7,6 @@ import (
"os"
"reflect"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apiserver"
"github.com/SigNoz/signoz/pkg/apiserver/signozapiserver"
"github.com/SigNoz/signoz/pkg/authz"
@@ -70,7 +69,6 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ factory.Handler }{},
struct{ cloudintegration.Handler }{},
struct{ rulestatehistory.Handler }{},
struct{ alertmanager.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
if err != nil {
return nil, err

View File

@@ -288,7 +288,6 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.RegistryHandler,
handlers.CloudIntegrationHandler,
handlers.RuleStateHistory,
handlers.AlertmanagerHandler,
),
)
}

View File

@@ -500,7 +500,7 @@ func New(
// Initialize all handlers for the modules
registryHandler := factory.NewHandler(registry)
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler, alertmanager)
handlers := NewHandlers(modules, providerSettings, analytics, querierHandler, licensing, global, flagger, gateway, telemetryMetadataStore, authz, zeus, registryHandler)
// Initialize the API server (after registry so it can access service health)
apiserverInstance, err := factory.NewProviderFromNamedMap(

View File

@@ -889,12 +889,7 @@ func (t *telemetryMetaStore) getMetricsKeys(ctx context.Context, fieldKeySelecto
// }
if fieldKeySelector.MetricContext != nil {
if fieldKeySelector.MetricContext.MetricName != "" {
fieldConds = append(fieldConds, sb.E("metric_name", fieldKeySelector.MetricContext.MetricName))
}
if fieldKeySelector.MetricContext.MetricNamespace != "" {
fieldConds = append(fieldConds, sb.Like("metric_name", escapeForLike(fieldKeySelector.MetricContext.MetricNamespace)+"%"))
}
fieldConds = append(fieldConds, sb.E("metric_name", fieldKeySelector.MetricContext.MetricName))
}
conds = append(conds, sb.And(fieldConds...))
@@ -982,12 +977,7 @@ func (t *telemetryMetaStore) getMeterSourceMetricKeys(ctx context.Context, field
fieldConds = append(fieldConds, sb.NotLike("attr_name", "\\_\\_%"))
if fieldKeySelector.MetricContext != nil {
if fieldKeySelector.MetricContext.MetricName != "" {
fieldConds = append(fieldConds, sb.E("metric_name", fieldKeySelector.MetricContext.MetricName))
}
if fieldKeySelector.MetricContext.MetricNamespace != "" {
fieldConds = append(fieldConds, sb.Like("metric_name", escapeForLike(fieldKeySelector.MetricContext.MetricNamespace)+"%"))
}
fieldConds = append(fieldConds, sb.E("metric_name", fieldKeySelector.MetricContext.MetricName))
}
conds = append(conds, sb.And(fieldConds...))
@@ -1081,8 +1071,8 @@ func enrichWithIntrinsicMetricKeys(keys map[string][]*telemetrytypes.TelemetryFi
if selector.Signal != telemetrytypes.SignalMetrics && selector.Signal != telemetrytypes.SignalUnspecified {
continue
}
// If metric filters are provided, do not surface intrinsic metric keys.
if selector.MetricContext != nil && (selector.MetricContext.MetricName != "" || selector.MetricContext.MetricNamespace != "") {
// If a metricName is provided, dont surface intrinsic metric keys
if selector.MetricContext != nil && selector.MetricContext.MetricName != "" {
continue
}
@@ -1738,12 +1728,9 @@ func (t *telemetryMetaStore) getMetricFieldValues(ctx context.Context, fieldValu
sb.Where(sb.E("attr_datatype", fieldValueSelector.FieldDataType.TagDataType()))
}
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricName != "" {
if fieldValueSelector.MetricContext != nil {
sb.Where(sb.E("metric_name", fieldValueSelector.MetricContext.MetricName))
}
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricNamespace != "" {
sb.Where(sb.Like("metric_name", escapeForLike(fieldValueSelector.MetricContext.MetricNamespace)+"%"))
}
if fieldValueSelector.StartUnixMilli > 0 {
sb.Where(sb.GE("last_reported_unix_milli", fieldValueSelector.StartUnixMilli))
@@ -1825,9 +1812,6 @@ func (t *telemetryMetaStore) getIntrinsicMetricFieldValues(ctx context.Context,
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricName != "" {
sb.Where(sb.E("metric_name", fieldValueSelector.MetricContext.MetricName))
}
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricNamespace != "" {
sb.Where(sb.Like("metric_name", escapeForLike(fieldValueSelector.MetricContext.MetricNamespace)+"%"))
}
if fieldValueSelector.StartUnixMilli > 0 {
sb.Where(sb.GE("unix_milli", fieldValueSelector.StartUnixMilli))
@@ -1885,13 +1869,6 @@ func (t *telemetryMetaStore) getMeterSourceMetricFieldValues(ctx context.Context
}
sb.Where(sb.NotLike("attr.1", "\\_\\_%"))
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricName != "" {
sb.Where(sb.E("metric_name", fieldValueSelector.MetricContext.MetricName))
}
if fieldValueSelector.MetricContext != nil && fieldValueSelector.MetricContext.MetricNamespace != "" {
sb.Where(sb.Like("metric_name", escapeForLike(fieldValueSelector.MetricContext.MetricNamespace)+"%"))
}
if fieldValueSelector.Value != "" {
if fieldValueSelector.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
sb.Where(sb.E("attr.2", fieldValueSelector.Value))

View File

@@ -320,20 +320,6 @@ func TestEnrichWithIntrinsicMetricKeys(t *testing.T) {
},
)
assert.NotContains(t, result, "metric_name")
result = enrichWithIntrinsicMetricKeys(
map[string][]*telemetrytypes.TelemetryFieldKey{},
[]*telemetrytypes.FieldKeySelector{
{
Signal: telemetrytypes.SignalMetrics,
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
},
},
)
assert.NotContains(t, result, "metric_name")
}
func TestGetMetricFieldValuesIntrinsicMetricName(t *testing.T) {
@@ -406,174 +392,3 @@ func TestGetMetricFieldValuesIntrinsicBoolReturnsEmpty(t *testing.T) {
assert.Empty(t, values.BoolValues)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestGetMetricFieldValuesAppliesMetricNamespace(t *testing.T) {
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &regexMatcher{})
mock := mockTelemetryStore.Mock()
metadata := newTestTelemetryMetaStoreTestHelper(mockTelemetryStore)
valueRows := cmock.NewRows([]cmock.ColumnType{
{Name: "attr_string_value", Type: "String"},
}, [][]any{{"value.a"}})
mock.ExpectQuery(regexp.QuoteMeta("SELECT DISTINCT attr_string_value FROM signoz_metrics.distributed_metadata WHERE attr_name = ? AND metric_name LIKE ? LIMIT ?")).
WithArgs("custom_key", "system.cpu%", 11).
WillReturnRows(valueRows)
values, complete, err := metadata.(*telemetryMetaStore).getMetricFieldValues(context.Background(), &telemetrytypes.FieldValueSelector{
FieldKeySelector: &telemetrytypes.FieldKeySelector{
Signal: telemetrytypes.SignalMetrics,
Name: "custom_key",
Limit: 10,
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
},
Limit: 10,
})
require.NoError(t, err)
assert.True(t, complete)
assert.ElementsMatch(t, []string{"value.a"}, values.StringValues)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestGetMetricFieldValuesIntrinsicMetricNameAppliesMetricNamespace(t *testing.T) {
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &regexMatcher{})
mock := mockTelemetryStore.Mock()
metadata := newTestTelemetryMetaStoreTestHelper(mockTelemetryStore)
valueRows := cmock.NewRows([]cmock.ColumnType{
{Name: "metric_name", Type: "String"},
}, [][]any{{"system.cpu.utilization"}})
mock.ExpectQuery(regexp.QuoteMeta("SELECT metric_name FROM signoz_metrics.distributed_time_series_v4_1week WHERE metric_name LIKE ? GROUP BY metric_name LIMIT ?")).
WithArgs("system.cpu%", 51).
WillReturnRows(valueRows)
metadataRows := cmock.NewRows([]cmock.ColumnType{
{Name: "attr_string_value", Type: "String"},
}, [][]any{})
mock.ExpectQuery(regexp.QuoteMeta("SELECT DISTINCT attr_string_value FROM signoz_metrics.distributed_metadata WHERE attr_name = ? AND metric_name LIKE ? LIMIT ?")).
WithArgs("metric_name", "system.cpu%", 50).
WillReturnRows(metadataRows)
values, complete, err := metadata.(*telemetryMetaStore).getMetricFieldValues(context.Background(), &telemetrytypes.FieldValueSelector{
FieldKeySelector: &telemetrytypes.FieldKeySelector{
Signal: telemetrytypes.SignalMetrics,
Name: "metric_name",
Limit: 50,
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
},
Limit: 50,
})
require.NoError(t, err)
assert.True(t, complete)
assert.ElementsMatch(t, []string{"system.cpu.utilization"}, values.StringValues)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestGetMeterSourceMetricFieldValuesAppliesMetricNamespace(t *testing.T) {
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &regexMatcher{})
mock := mockTelemetryStore.Mock()
metadata := newTestTelemetryMetaStoreTestHelper(mockTelemetryStore)
rows := cmock.NewRows([]cmock.ColumnType{
{Name: "attr", Type: "Array(String)"},
}, [][]any{{[]string{"service.name", "frontend"}}})
mock.ExpectQuery(`SELECT .*distributed_samples_agg_1d.*metric_name LIKE .*`).
WithArgs("service.name", "\\_\\_%", "system.cpu%", "", 11).
WillReturnRows(rows)
values, complete, err := metadata.(*telemetryMetaStore).getMeterSourceMetricFieldValues(context.Background(), &telemetrytypes.FieldValueSelector{
FieldKeySelector: &telemetrytypes.FieldKeySelector{
Signal: telemetrytypes.SignalMetrics,
Source: telemetrytypes.SourceMeter,
Name: "service.name",
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
},
Limit: 10,
})
require.NoError(t, err)
assert.True(t, complete)
assert.ElementsMatch(t, []string{"frontend"}, values.StringValues)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestGetMetricsKeysAppliesMetricNamespace(t *testing.T) {
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &regexMatcher{})
mock := mockTelemetryStore.Mock()
metadata := newTestTelemetryMetaStoreTestHelper(mockTelemetryStore)
rows := cmock.NewRows([]cmock.ColumnType{
{Name: "name", Type: "String"},
{Name: "field_context", Type: "String"},
{Name: "field_data_type", Type: "String"},
{Name: "priority", Type: "UInt8"},
}, [][]any{{"service.name", "resource", "String", 1}})
mock.ExpectQuery(`(?s)SELECT.*distributed_metadata.*metric_name LIKE.*`).
WithArgs("%service%", "\\_\\_%", "system.cpu%", 11).
WillReturnRows(rows)
keys, complete, err := metadata.(*telemetryMetaStore).getMetricsKeys(context.Background(), []*telemetrytypes.FieldKeySelector{
{
Signal: telemetrytypes.SignalMetrics,
Name: "service",
Limit: 10,
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
},
})
require.NoError(t, err)
assert.True(t, complete)
assert.Len(t, keys, 1)
assert.Equal(t, "service.name", keys[0].Name)
require.NoError(t, mock.ExpectationsWereMet())
}
func TestGetMeterSourceMetricKeysAppliesMetricNamespace(t *testing.T) {
mockTelemetryStore := telemetrystoretest.New(telemetrystore.Config{}, &regexMatcher{})
mock := mockTelemetryStore.Mock()
metadata := newTestTelemetryMetaStoreTestHelper(mockTelemetryStore)
rows := cmock.NewRows([]cmock.ColumnType{
{Name: "attr_name", Type: "String"},
}, [][]any{{"service.name"}})
mock.ExpectQuery(`SELECT.*distributed_samples_agg_1d.*metric_name LIKE.*`).
WithArgs("%service%", "\\_\\_%", "system.cpu%", 10).
WillReturnRows(rows)
keys, complete, err := metadata.(*telemetryMetaStore).getMeterSourceMetricKeys(context.Background(), []*telemetrytypes.FieldKeySelector{
{
Signal: telemetrytypes.SignalMetrics,
Source: telemetrytypes.SourceMeter,
Name: "service",
Limit: 10,
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
MetricContext: &telemetrytypes.MetricContext{
MetricNamespace: "system.cpu",
},
},
})
require.NoError(t, err)
assert.True(t, complete)
assert.Len(t, keys, 1)
assert.Equal(t, "service.name", keys[0].Name)
require.NoError(t, mock.ExpectationsWereMet())
}

View File

@@ -34,10 +34,10 @@ type Channel struct {
types.Identifiable
types.TimeAuditable
Name string `json:"name" required:"true" bun:"name"`
Type string `json:"type" required:"true" bun:"type"`
Data string `json:"data" required:"true" bun:"data"`
OrgID string `json:"orgId" required:"true" bun:"org_id"`
Name string `json:"name" bun:"name"`
Type string `json:"type" bun:"type"`
Data string `json:"data" bun:"data"`
OrgID string `json:"org_id" bun:"org_id"`
}
// NewChannelFromReceiver creates a new Channel from a Receiver.

View File

@@ -12,12 +12,12 @@ import (
)
type PostableRoutePolicy struct {
Expression string `json:"expression" required:"true"`
Expression string `json:"expression"`
ExpressionKind ExpressionKind `json:"kind"`
Channels []string `json:"channels" required:"true"`
Name string `json:"name" required:"true"`
Channels []string `json:"channels"`
Name string `json:"name"`
Description string `json:"description"`
Tags []string `json:"tags,omitempty" nullable:"true"`
Tags []string `json:"tags,omitempty"`
}
func (p *PostableRoutePolicy) Validate() error {
@@ -53,13 +53,15 @@ func (p *PostableRoutePolicy) Validate() error {
}
type GettableRoutePolicy struct {
PostableRoutePolicy
PostableRoutePolicy // Embedded
ID string `json:"id" required:"true"`
CreatedAt time.Time `json:"createdAt" required:"true"`
UpdatedAt time.Time `json:"updatedAt" required:"true"`
CreatedBy *string `json:"createdBy" nullable:"true"`
UpdatedBy *string `json:"updatedBy" nullable:"true"`
ID string `json:"id"`
// Audit fields
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
CreatedBy *string `json:"createdBy"`
UpdatedBy *string `json:"updatedBy"`
}
type ExpressionKind struct {
@@ -71,13 +73,6 @@ var (
PolicyBasedExpression = ExpressionKind{valuer.NewString("policy")}
)
func (ExpressionKind) Enum() []any {
return []any{
RuleBasedExpression,
PolicyBasedExpression,
}
}
// RoutePolicy represents the database model for expression routes.
type RoutePolicy struct {
bun.BaseModel `bun:"table:route_policy"`

View File

@@ -268,8 +268,7 @@ func (t *TelemetryFieldValues) NumValues() int {
}
type MetricContext struct {
MetricName string `json:"metricName"`
MetricNamespace string `json:"metricNamespace,omitempty"`
MetricName string `json:"metricName"`
}
type FieldKeySelector struct {
@@ -298,16 +297,15 @@ type GettableFieldKeys struct {
}
type PostableFieldKeysParams struct {
Signal Signal `query:"signal"`
Source Source `query:"source"`
Limit int `query:"limit"`
StartUnixMilli int64 `query:"startUnixMilli"`
EndUnixMilli int64 `query:"endUnixMilli"`
FieldContext FieldContext `query:"fieldContext"`
FieldDataType FieldDataType `query:"fieldDataType"`
MetricName string `query:"metricName"`
MetricNamespace string `query:"metricNamespace"`
SearchText string `query:"searchText"`
Signal Signal `query:"signal"`
Source Source `query:"source"`
Limit int `query:"limit"`
StartUnixMilli int64 `query:"startUnixMilli"`
EndUnixMilli int64 `query:"endUnixMilli"`
FieldContext FieldContext `query:"fieldContext"`
FieldDataType FieldDataType `query:"fieldDataType"`
MetricName string `query:"metricName"`
SearchText string `query:"searchText"`
}
type GettableFieldValues struct {
@@ -346,10 +344,9 @@ func NewFieldKeySelectorFromPostableFieldKeysParams(params PostableFieldKeysPara
req.Limit = 1000
}
if params.MetricName != "" || params.MetricNamespace != "" {
if params.MetricName != "" {
req.MetricContext = &MetricContext{
MetricName: params.MetricName,
MetricNamespace: params.MetricNamespace,
MetricName: params.MetricName,
}
}

View File

@@ -394,20 +394,3 @@ func TestNormalize(t *testing.T) {
})
}
}
func TestNewFieldKeySelectorFromPostableFieldKeysParamsMetricNamespace(t *testing.T) {
selector := NewFieldKeySelectorFromPostableFieldKeysParams(PostableFieldKeysParams{
Signal: SignalMetrics,
MetricNamespace: "system.cpu",
})
if selector.MetricContext == nil {
t.Fatalf("expected metric context to be set")
}
if selector.MetricContext.MetricNamespace != "system.cpu" {
t.Fatalf("expected metric namespace to be propagated, got %q", selector.MetricContext.MetricNamespace)
}
if selector.MetricContext.MetricName != "" {
t.Fatalf("expected metric name to remain empty, got %q", selector.MetricContext.MetricName)
}
}

View File

@@ -658,211 +658,3 @@ def test_non_existent_metrics_returns_404(
get_error_message(response.json())
== "could not find the metric whatevergoennnsgoeshere"
)
# Verify /api/v1/fields/values filters label values by metricNamespace prefix.
# Inserts metrics under ns.a and ns.b, then asserts a specific prefix returns
# only matching values while a common prefix returns both.
def test_metric_namespace_values_filtering(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
metrics: List[Metrics] = [
Metrics(
metric_name="ns.a.requests_total",
labels={"service": "svc-a"},
timestamp=now - timedelta(minutes=2),
value=10.0,
),
Metrics(
metric_name="ns.b.requests_total",
labels={"service": "svc-b"},
timestamp=now - timedelta(minutes=2),
value=20.0,
),
]
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Specific prefix: metricNamespace=ns.a should return only svc-a
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/fields/values"),
timeout=5,
headers={"authorization": f"Bearer {token}"},
params={
"signal": "metrics",
"name": "service",
"searchText": "",
"metricNamespace": "ns.a",
},
)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
values = response.json()["data"]["values"]["stringValues"]
assert "svc-a" in values
assert "svc-b" not in values
# Common prefix: metricNamespace=ns should return both
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/fields/values"),
timeout=5,
headers={"authorization": f"Bearer {token}"},
params={
"signal": "metrics",
"name": "service",
"searchText": "",
"metricNamespace": "ns",
},
)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
values = response.json()["data"]["values"]["stringValues"]
assert "svc-a" in values
assert "svc-b" in values
# Verify /api/v1/fields/values with name=metric_name filters metric names by
# metricNamespace prefix. A specific prefix returns only its metric names;
# a common prefix returns metric names from all matching namespaces.
def test_metric_namespace_metric_name_values_filtering(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
metrics: List[Metrics] = [
Metrics(
metric_name="ns.a.cpu.utilization",
labels={"host": "host-a"},
timestamp=now - timedelta(minutes=2),
value=50.0,
),
Metrics(
metric_name="ns.b.cpu.utilization",
labels={"host": "host-b"},
timestamp=now - timedelta(minutes=2),
value=60.0,
),
]
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Specific prefix: metricNamespace=ns.a should return only ns.a.* metric names
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/fields/values"),
timeout=5,
headers={"authorization": f"Bearer {token}"},
params={
"signal": "metrics",
"name": "metric_name",
"searchText": "",
"metricNamespace": "ns.a",
},
)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
values = response.json()["data"]["values"]["stringValues"]
assert "ns.a.cpu.utilization" in values
assert "ns.b.cpu.utilization" not in values
# Common prefix: metricNamespace=ns should return both
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/fields/values"),
timeout=5,
headers={"authorization": f"Bearer {token}"},
params={
"signal": "metrics",
"name": "metric_name",
"searchText": "",
"metricNamespace": "ns",
},
)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
values = response.json()["data"]["values"]["stringValues"]
assert "ns.a.cpu.utilization" in values
assert "ns.b.cpu.utilization" in values
# Verify /api/v1/fields/keys filters attribute keys by metricNamespace prefix.
# Metrics under ns.a and ns.b carry distinct labels; a specific prefix returns
# only its keys while a common prefix returns keys from both namespaces.
def test_metric_namespace_keys_filtering(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
metrics: List[Metrics] = [
Metrics(
metric_name="ns.a.cpu.utilization",
labels={"a_only_label": "val-a"},
timestamp=now - timedelta(minutes=2),
value=10.0,
),
Metrics(
metric_name="ns.b.cpu.utilization",
labels={"b_only_label": "val-b"},
timestamp=now - timedelta(minutes=2),
value=20.0,
),
]
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Specific prefix: metricNamespace=ns.a should return only a_only_label
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/fields/keys"),
timeout=5,
headers={"authorization": f"Bearer {token}"},
params={
"signal": "metrics",
"searchText": "label",
"metricNamespace": "ns.a",
},
)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
keys = response.json()["data"]["keys"]
assert "a_only_label" in keys
assert "b_only_label" not in keys
# Common prefix: metricNamespace=ns should return both keys
response = requests.get(
signoz.self.host_configs["8080"].get("/api/v1/fields/keys"),
timeout=5,
headers={"authorization": f"Bearer {token}"},
params={
"signal": "metrics",
"searchText": "label",
"metricNamespace": "ns",
},
)
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
keys = response.json()["data"]["keys"]
assert "a_only_label" in keys
assert "b_only_label" in keys

Some files were not shown because too many files have changed in this diff Show More