Compare commits

..

21 Commits

Author SHA1 Message Date
SagarRajput-7
7a23cb08be feat(base-path): merge conflict fix 2026-04-21 10:20:39 +05:30
SagarRajput-7
854b721164 feat: code refactor around feedbacks 2026-04-21 10:17:46 +05:30
SagarRajput-7
c7b5cc6a71 feat: applied suggested patch changes 2026-04-21 10:17:46 +05:30
SagarRajput-7
0103fb868f feat: code refactor around feedbacks 2026-04-21 10:17:43 +05:30
SagarRajput-7
7fbf1eb38a feat: updated base path utils and fixed navigation and translations 2026-04-21 10:17:21 +05:30
SagarRajput-7
afbbaa023b feat: updated the html template 2026-04-21 10:17:21 +05:30
SagarRajput-7
2bf96ea5b1 feat: removed plugin and serving the index.html only as the template 2026-04-21 10:17:21 +05:30
SagarRajput-7
20ab3a8ccb feat: refactor the interceptor and added gotmpl into gitignore 2026-04-21 10:17:21 +05:30
SagarRajput-7
3fc2a367f9 feat: changed output path to dir level 2026-04-21 10:17:21 +05:30
SagarRajput-7
3759a2dfbf feat: base path config setup and plugin for gotmpl generation at build time 2026-04-21 10:17:21 +05:30
grandwizard28
28eaf4994e fix: remove unused files 2026-04-21 10:17:01 +05:30
grandwizard28
81fb9660dc style: formatting and test cleanup from review
Restructure Validate nil check, rename expectErr to fail with
early-return, trim trailing newlines in test assertions, remove
t.Parallel from subtests, inline short config literals, restore
struct field comments in web.Config.
2026-04-21 10:17:01 +05:30
grandwizard28
acb9b6a5cd fix(web): resolve lint errors in provider and template
Fix errcheck on rw.Write in serveIndex, use ErrorContext instead of
Error in NewIndex for sloglint compliance. Move serveIndex below
ServeHTTP to order public methods before private ones.
2026-04-21 10:16:53 +05:30
grandwizard28
e58803eafe style: add blank lines between logical blocks 2026-04-21 10:16:42 +05:30
grandwizard28
fc6c0ae861 refactor(web): rename test fixtures to no_template, valid_template, invalid_template
Drop the index_ prefix from test fixtures. Use web instead of w for
the variable name in test helpers.
2026-04-21 10:15:16 +05:30
grandwizard28
0bbe5d6698 test(web): add no-template and invalid-template index test cases
Add three distinct index fixtures in testdata:
- index.html: correct [[ ]] template with BaseHref
- index_no_template.html: plain HTML, no placeholders
- index_invalid_template.html: malformed template syntax

Tests verify: template substitution works, plain files pass through
unchanged, and invalid templates fall back to serving raw bytes.
Consolidate test helpers into startServer/get.
2026-04-21 10:14:43 +05:30
grandwizard28
6c74b7bc2e refactor(web): collapse testdata_basepath into testdata
Use a single testdata directory with a templated index.html for all
routerweb tests. Remove the redundant testdata_basepath directory.
2026-04-21 10:14:23 +05:30
grandwizard28
bbb82d2d2e refactor(global): rename RoutePrefix to ExternalPath, add ExternalPathTrailing
Rename RoutePrefix() to ExternalPath() to accurately reflect what it
returns: the path component of the external URL. Add
ExternalPathTrailing() which returns the path with a trailing slash,
used for HTML base href injection.
2026-04-21 10:12:39 +05:30
grandwizard28
1b5f8f5581 refactor(web): extract index.html templating into web.NewIndex
Move the template parsing and execution logic from routerweb provider
into pkg/web/template.go. NewIndex logs and returns raw bytes on
template failure; NewIndexE returns the error for callers that need it.

Rename BasePath to BaseHref to match the HTML attribute it populates.
Inject global.Config into routerweb via the factory closure pattern.
2026-04-21 10:12:14 +05:30
grandwizard28
2299e01b3d feat(web): template index.html with dynamic base href from global.external_url
Read index.html at startup, parse as Go template with [[ ]] delimiters,
execute with BasePath derived from global.external_url, and cache the
rendered bytes in memory. This injects <base href="/signoz/" /> (or
whatever the route prefix is) so the browser resolves relative URLs
correctly when SigNoz is served at a sub-path.

Inject global.Config into the routerweb provider via the factory closure
pattern. Static files (JS, CSS, images) are still served from disk
unchanged.
2026-04-21 10:11:52 +05:30
grandwizard28
73024389da feat(apiserver): derive HTTP route prefix from global.external_url
The path component of global.external_url is now used as the base path
for all HTTP routes (API and web frontend), enabling SigNoz to be served
behind a reverse proxy at a sub-path (e.g. https://example.com/signoz/).

The prefix is applied via http.StripPrefix at the outermost handler
level, requiring zero changes to route registration code. Health
endpoints (/api/v1/health, /api/v2/healthz, /api/v2/readyz,
/api/v2/livez) remain accessible without the prefix for container
healthchecks.

Removes web.prefix config in favor of the unified global.external_url
approach, avoiding the desync bugs seen in projects with separate
API/UI prefix configs (ArgoCD, Prometheus).

closes SigNoz/platform-pod#1775
2026-04-21 10:10:54 +05:30
107 changed files with 850 additions and 950 deletions

View File

@@ -66,8 +66,8 @@ module.exports = {
rules: {
// Asset migration — base-path safety
'rulesdir/no-unsupported-asset-pattern': 'error',
// Base-path safety — window.open and origin-concat patterns
'rulesdir/no-raw-absolute-path': 'error',
// Base-path safety — window.open and origin-concat patterns; upgrade to error coming PR
'rulesdir/no-raw-absolute-path': 'warn',
// Code quality rules
'prefer-const': 'error', // Enforces const for variables never reassigned
@@ -215,31 +215,6 @@ module.exports = {
message:
'Avoid calling .getState() directly. Export a standalone action from the store instead.',
},
{
selector:
"MemberExpression[object.name='window'][property.name='localStorage']",
message:
'Use getLocalStorageKey/setLocalStorageKey/removeLocalStorageKey from api/browser/localstorage instead.',
},
{
selector:
"MemberExpression[object.name='window'][property.name='sessionStorage']",
message:
'Use getSessionStorageApi/setSessionStorageApi/removeSessionStorageApi from api/browser/sessionstorage instead.',
},
],
'no-restricted-globals': [
'error',
{
name: 'localStorage',
message:
'Use getLocalStorageKey/setLocalStorageKey/removeLocalStorageKey from api/browser/localstorage instead.',
},
{
name: 'sessionStorage',
message:
'Use getSessionStorageApi/setSessionStorageApi/removeSessionStorageApi from api/browser/sessionstorage instead.',
},
],
},
overrides: [
@@ -273,11 +248,6 @@ module.exports = {
'sonarjs/cognitive-complexity': 'off', // Tests can be complex
'sonarjs/no-identical-functions': 'off', // Similar test patterns are OK
'sonarjs/no-small-switch': 'off', // Small switches are OK in tests
// Test assertions intentionally reference window.location.origin for expected-value checks
'rulesdir/no-raw-absolute-path': 'off',
// Tests may access storage directly for setup/assertion/spy purposes
'no-restricted-globals': 'off',
'no-restricted-syntax': 'off',
},
},
{

View File

@@ -40,12 +40,12 @@
<meta
data-react-helmet="true"
property="og:image"
content="[[.BaseHref]]images/signoz-hero-image.webp"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
name="twitter:image"
content="[[.BaseHref]]images/signoz-hero-image.webp"
content="/images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"

View File

@@ -1,9 +1,6 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const get = (key: string): string | null => {
try {
return localStorage.getItem(getScopedKey(key));
return localStorage.getItem(key);
} catch (e) {
return '';
}

View File

@@ -1,9 +1,6 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const remove = (key: string): boolean => {
try {
localStorage.removeItem(getScopedKey(key));
window.localStorage.removeItem(key);
return true;
} catch (e) {
return false;

View File

@@ -1,9 +1,6 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const set = (key: string, value: string): boolean => {
try {
localStorage.setItem(getScopedKey(key), value);
localStorage.setItem(key, value);
return true;
} catch (e) {
return false;

View File

@@ -1,12 +0,0 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const get = (key: string): string | null => {
try {
return sessionStorage.getItem(getScopedKey(key));
} catch (e) {
return '';
}
};
export default get;

View File

@@ -1,13 +0,0 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const remove = (key: string): boolean => {
try {
sessionStorage.removeItem(getScopedKey(key));
return true;
} catch (e) {
return false;
}
};
export default remove;

View File

@@ -1,13 +0,0 @@
/* eslint-disable no-restricted-globals */
import { getScopedKey } from 'utils/storage';
const set = (key: string, value: string): boolean => {
try {
sessionStorage.setItem(getScopedKey(key), value);
return true;
} catch (e) {
return false;
}
};
export default set;

View File

@@ -3,16 +3,13 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
import { ENVIRONMENT } from 'constants/env';
import { LOCALSTORAGE } from 'constants/localStorage';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { withBasePath } from 'utils/basePath';
// 10 min in ms
const TIMEOUT_IN_MS = 10 * 60 * 1000;
export const LiveTail = (queryParams: string): EventSourcePolyfill =>
new EventSourcePolyfill(
ENVIRONMENT.baseURL
? `${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`
: withBasePath(`${apiV1}logs/tail?${queryParams}`),
`${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`,
{
headers: {
Authorization: `Bearer ${getLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN)}`,

View File

@@ -9,7 +9,6 @@ import { CreditCard, MessageSquareText, X } from 'lucide-react';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
export default function ChatSupportGateway(): JSX.Element {
const { notifications } = useNotifications();
@@ -55,7 +54,7 @@ export default function ChatSupportGateway(): JSX.Element {
});
updateCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
};

View File

@@ -31,7 +31,6 @@ import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useTimezone } from 'providers/Timezone';
import APIError from 'types/api/error';
import { getAbsoluteUrl } from 'utils/basePath';
import { toAPIError } from 'utils/errorUtils';
import DeleteMemberDialog from './DeleteMemberDialog';
@@ -388,7 +387,7 @@ function EditMemberDrawer({
pathParams: { id: member.id },
});
if (response?.data?.token) {
const link = getAbsoluteUrl(`/password-reset?token=${response.data.token}`);
const link = `${window.location.origin}/password-reset?token=${response.data.token}`;
setResetLink(link);
setResetLinkExpiresAt(
response.data.expiresAt

View File

@@ -13,7 +13,6 @@ import GetMinMax from 'lib/getMinMax';
import { Check, Info, Link2 } from 'lucide-react';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getAbsoluteUrl } from 'utils/basePath';
const routesToBeSharedWithTime = [
ROUTES.LOGS_EXPLORER,
@@ -81,13 +80,17 @@ function ShareURLModal(): JSX.Element {
urlQuery.delete(QueryParams.relativeTime);
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
currentUrl = `${window.location.origin}${
location.pathname
}?${urlQuery.toString()}`;
} else {
urlQuery.delete(QueryParams.startTime);
urlQuery.delete(QueryParams.endTime);
urlQuery.set(QueryParams.relativeTime, selectedTime);
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
currentUrl = `${window.location.origin}${
location.pathname
}?${urlQuery.toString()}`;
}
}

View File

@@ -14,7 +14,6 @@ import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { ROLES } from 'types/roles';
import { EMAIL_REGEX } from 'utils/app';
import { getBaseUrl } from 'utils/basePath';
import { popupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
@@ -189,7 +188,7 @@ function InviteMembersModal({
email: row.email.trim(),
name: '',
role: row.role as ROLES,
frontendBaseUrl: getBaseUrl(),
frontendBaseUrl: window.location.origin,
});
} else {
await inviteUsers({
@@ -197,7 +196,7 @@ function InviteMembersModal({
email: row.email.trim(),
name: '',
role: row.role,
frontendBaseUrl: getBaseUrl(),
frontendBaseUrl: window.location.origin,
})),
});
}

View File

@@ -14,7 +14,6 @@ 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 { getBaseUrl } from 'utils/basePath';
import './LaunchChatSupport.styles.scss';
@@ -155,7 +154,7 @@ function LaunchChatSupport({
});
updateCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
};

View File

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

View File

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

View File

@@ -1,13 +1,10 @@
import getSessionStorageApi from 'api/browser/sessionstorage/get';
import removeSessionStorageApi from 'api/browser/sessionstorage/remove';
import setSessionStorageApi from 'api/browser/sessionstorage/set';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export const PREVIOUS_QUERY_KEY = 'previousQuery';
function getPreviousQueryFromStore(): Record<string, IBuilderQuery> {
try {
const raw = getSessionStorageApi(PREVIOUS_QUERY_KEY);
const raw = sessionStorage.getItem(PREVIOUS_QUERY_KEY);
if (!raw) {
return {};
}
@@ -20,7 +17,7 @@ function getPreviousQueryFromStore(): Record<string, IBuilderQuery> {
function writePreviousQueryToStore(store: Record<string, IBuilderQuery>): void {
try {
setSessionStorageApi(PREVIOUS_QUERY_KEY, JSON.stringify(store));
sessionStorage.setItem(PREVIOUS_QUERY_KEY, JSON.stringify(store));
} catch {
// ignore quota or serialization errors
}
@@ -66,7 +63,7 @@ export const removeKeyFromPreviousQuery = (key: string): void => {
export const clearPreviousQuery = (): void => {
try {
removeSessionStorageApi(PREVIOUS_QUERY_KEY);
sessionStorage.removeItem(PREVIOUS_QUERY_KEY);
} catch {
// no-op
}

View File

@@ -108,7 +108,8 @@ function DynamicColumnTable({
// Update URL with new page number while preserving other params
urlQuery.set('page', page.toString());
safeNavigate({ search: `?${urlQuery.toString()}` });
const newUrl = `${window.location.pathname}?${urlQuery.toString()}`;
safeNavigate(newUrl);
// Call original pagination handler if provided
if (pagination?.onChange && !!pageSize) {

View File

@@ -1,6 +1,3 @@
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { DynamicColumnsKey } from './contants';
import {
GetNewColumnDataFunction,
@@ -15,7 +12,7 @@ export const getVisibleColumns: GetVisibleColumnsFunction = ({
}) => {
let columnVisibilityData: { [key: string]: boolean };
try {
const storedData = getLocalStorageKey(tablesource);
const storedData = localStorage.getItem(tablesource);
if (typeof storedData === 'string' && dynamicColumns) {
columnVisibilityData = JSON.parse(storedData);
return dynamicColumns.filter((column) => {
@@ -31,7 +28,7 @@ export const getVisibleColumns: GetVisibleColumnsFunction = ({
initialColumnVisibility[key] = false;
});
setLocalStorageKey(tablesource, JSON.stringify(initialColumnVisibility));
localStorage.setItem(tablesource, JSON.stringify(initialColumnVisibility));
} catch (error) {
console.error(error);
}
@@ -45,14 +42,14 @@ export const setVisibleColumns = ({
dynamicColumns,
}: SetVisibleColumnsProps): void => {
try {
const storedData = getLocalStorageKey(tablesource);
const storedData = localStorage.getItem(tablesource);
if (typeof storedData === 'string' && dynamicColumns) {
const columnVisibilityData = JSON.parse(storedData);
const { key } = dynamicColumns[index];
if (key) {
columnVisibilityData[key] = checked;
}
setLocalStorageKey(tablesource, JSON.stringify(columnVisibilityData));
localStorage.setItem(tablesource, JSON.stringify(columnVisibilityData));
}
} catch (error) {
console.error(error);

View File

@@ -9,7 +9,6 @@ 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';
@@ -95,14 +94,20 @@ function DependentServices({
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
const serviceName =
record.serviceData.serviceName && record.serviceData.serviceName !== '-'
? record.serviceData.serviceName
: '';
const url = new URL(
`/services/${
record.serviceData.serviceName &&
record.serviceData.serviceName !== '-'
? record.serviceData.serviceName
: ''
}`,
window.location.origin,
);
const urlQuery = new URLSearchParams();
urlQuery.set(QueryParams.startTime, timeRange.startTime.toString());
urlQuery.set(QueryParams.endTime, timeRange.endTime.toString());
openInNewTab(`/services/${serviceName}?${urlQuery.toString()}`);
url.search = urlQuery.toString();
window.open(url.toString(), '_blank');
},
className: 'clickable-row',
})}

View File

@@ -73,7 +73,6 @@ import {
import { UserPreference } from 'types/api/preferences/preference';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { getBaseUrl } from 'utils/basePath';
import { showErrorNotification } from 'utils/error';
import { eventEmitter } from 'utils/getEventEmitter';
import {
@@ -462,7 +461,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const handleFailedPayment = useCallback((): void => {
manageCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
}, [manageCreditCard]);

View File

@@ -31,7 +31,6 @@ import { isEmpty, pick } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { getBaseUrl } from 'utils/basePath';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
@@ -325,7 +324,7 @@ export default function BillingContainer(): JSX.Element {
});
updateCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
} else {
logEvent('Billing : Manage Billing', {
@@ -334,7 +333,7 @@ export default function BillingContainer(): JSX.Element {
});
manageCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

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

View File

@@ -14,7 +14,6 @@ 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';
@@ -388,7 +387,7 @@ export function NotificationChannelsNotFoundContent({
style={{ padding: '0 4px' }}
type="link"
onClick={(): void => {
openInNewTab(ROUTES.CHANNELS_NEW);
window.open(ROUTES.CHANNELS_NEW, '_blank');
}}
>
here.

View File

@@ -48,7 +48,6 @@ function DomainUpdateToast({
className="custom-domain-toast-visit-btn"
suffixIcon={<ExternalLink size={12} />}
onClick={(): void => {
// eslint-disable-next-line rulesdir/no-raw-absolute-path
window.open(url, '_blank', 'noopener,noreferrer');
}}
>

View File

@@ -16,8 +16,6 @@ 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 { getAbsoluteUrl } from 'utils/basePath';
import { openInNewTab } from 'utils/navigation';
import './PublicDashboard.styles.scss';
@@ -215,7 +213,7 @@ function PublicDashboardSetting(): JSX.Element {
try {
setCopyPublicDashboardURL(
getAbsoluteUrl(publicDashboardResponse?.data?.publicPath ?? ''),
`${window.location.origin}${publicDashboardResponse?.data?.publicPath}`,
);
toast.success('Copied Public Dashboard URL successfully');
} catch (error) {
@@ -224,7 +222,7 @@ function PublicDashboardSetting(): JSX.Element {
};
const publicDashboardURL = useMemo(
() => getAbsoluteUrl(publicDashboardResponse?.data?.publicPath ?? ''),
() => `${window.location.origin}${publicDashboardResponse?.data?.publicPath}`,
[publicDashboardResponse],
);
@@ -296,7 +294,7 @@ function PublicDashboardSetting(): JSX.Element {
icon={<ExternalLink size={12} />}
onClick={(): void => {
if (publicDashboardURL) {
openInNewTab(publicDashboardURL);
window.open(publicDashboardURL, '_blank');
}
}}
/>

View File

@@ -1,6 +1,5 @@
import { useCallback, useRef } from 'react';
import { Button } from 'antd';
import getSessionStorageApi from 'api/browser/sessionstorage/get';
import ROUTES from 'constants/routes';
import { DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY } from 'hooks/dashboard/useDashboardsListQueryParams';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
@@ -27,7 +26,7 @@ function DashboardBreadcrumbs(): JSX.Element {
const { title = '', image = Base64Icons[0] } = selectedData || {};
const goToListPage = useCallback(() => {
const dashboardsListQueryParamsString = getSessionStorageApi(
const dashboardsListQueryParamsString = sessionStorage.getItem(
DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY,
);

View File

@@ -1,6 +1,3 @@
import getLocalStorageKey from 'api/browser/localstorage/get';
import removeLocalStorageKey from 'api/browser/localstorage/remove';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { LOCALSTORAGE } from 'constants/localStorage';
import { GraphVisibilityState, SeriesVisibilityItem } from '../types';
@@ -15,7 +12,7 @@ export function getStoredSeriesVisibility(
widgetId: string,
): SeriesVisibilityItem[] | null {
try {
const storedData = getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
const storedData = localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
if (!storedData) {
return null;
@@ -32,7 +29,7 @@ export function getStoredSeriesVisibility(
} catch (error) {
if (error instanceof SyntaxError) {
// If the stored data is malformed, remove it
removeLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
localStorage.removeItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
}
// Silently handle parsing errors - fall back to default visibility
return null;
@@ -45,7 +42,7 @@ export function updateSeriesVisibilityToLocalStorage(
): void {
let visibilityStates: GraphVisibilityState[] = [];
try {
const storedData = getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
const storedData = localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
visibilityStates = JSON.parse(storedData || '[]');
} catch (error) {
if (error instanceof SyntaxError) {
@@ -66,7 +63,7 @@ export function updateSeriesVisibilityToLocalStorage(
];
}
setLocalStorageKey(
localStorage.setItem(
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
JSON.stringify(visibilityStates),
);

View File

@@ -22,8 +22,6 @@ import {
Tooltip,
Typography,
} from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import { TelemetryFieldKey } from 'api/v5/v5';
import axios from 'axios';
@@ -474,7 +472,7 @@ function ExplorerOptions({
value: string;
}): void => {
// Retrieve stored views from local storage
const storedViews = getLocalStorageKey(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
const storedViews = localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
// Initialize or parse the stored views
const updatedViews: PreservedViewsInLocalStorage = storedViews
@@ -488,7 +486,7 @@ function ExplorerOptions({
};
// Save the updated views back to local storage
setLocalStorageKey(
localStorage.setItem(
PRESERVED_VIEW_LOCAL_STORAGE_KEY,
JSON.stringify(updatedViews),
);
@@ -539,7 +537,7 @@ function ExplorerOptions({
const removeCurrentViewFromLocalStorage = (): void => {
// Retrieve stored views from local storage
const storedViews = getLocalStorageKey(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
const storedViews = localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY);
if (storedViews) {
// Parse the stored views
@@ -549,7 +547,7 @@ function ExplorerOptions({
delete parsedViews[PRESERVED_VIEW_TYPE];
// Update local storage with the modified views
setLocalStorageKey(
localStorage.setItem(
PRESERVED_VIEW_LOCAL_STORAGE_KEY,
JSON.stringify(parsedViews),
);
@@ -674,7 +672,7 @@ function ExplorerOptions({
}
const parsedPreservedView = JSON.parse(
getLocalStorageKey(PRESERVED_VIEW_LOCAL_STORAGE_KEY) || '{}',
localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY) || '{}',
);
const preservedView = parsedPreservedView[PRESERVED_VIEW_TYPE] || {};

View File

@@ -1,6 +1,4 @@
import { Color } from '@signozhq/design-tokens';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { showErrorNotification } from 'components/ExplorerCard/utils';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
@@ -73,7 +71,7 @@ export const generateRGBAFromHex = (hex: string, opacity: number): string =>
export const getExplorerToolBarVisibility = (dataSource: string): boolean => {
try {
const showExplorerToolbar = getLocalStorageKey(
const showExplorerToolbar = localStorage.getItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
);
if (showExplorerToolbar === null) {
@@ -86,7 +84,7 @@ export const getExplorerToolBarVisibility = (dataSource: string): boolean => {
[DataSource.TRACES]: true,
[DataSource.LOGS]: true,
};
setLocalStorageKey(
localStorage.setItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
JSON.stringify(parsedShowExplorerToolbar),
);
@@ -105,13 +103,13 @@ export const setExplorerToolBarVisibility = (
dataSource: string,
): void => {
try {
const showExplorerToolbar = getLocalStorageKey(
const showExplorerToolbar = localStorage.getItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
);
if (showExplorerToolbar) {
const parsedShowExplorerToolbar = JSON.parse(showExplorerToolbar);
parsedShowExplorerToolbar[dataSource] = value;
setLocalStorageKey(
localStorage.setItem(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
JSON.stringify(parsedShowExplorerToolbar),
);

View File

@@ -10,7 +10,6 @@ import ROUTES from 'constants/routes';
import history from 'lib/history';
import APIError from 'types/api/error';
import { OrgSessionContext } from 'types/api/v2/sessions/context/get';
import { getBaseUrl } from 'utils/basePath';
import tvUrl from '@/assets/svgs/tv.svg';
@@ -106,7 +105,7 @@ function ForgotPassword({
data: {
email: values.email,
orgId: currentOrgId,
frontendBaseURL: getBaseUrl(),
frontendBaseURL: window.location.origin,
},
});
}, [form, forgotPasswordMutate, initialOrgId, hasMultipleOrgs]);

View File

@@ -15,7 +15,6 @@ 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';
@@ -88,7 +87,7 @@ function BasicInfo({
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
ruleId: isNewRule ? 0 : alertDef?.id,
});
openInNewTab(ROUTES.CHANNELS_NEW);
window.open(ROUTES.CHANNELS_NEW, '_blank');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const hasLoggedEvent = useRef(false);

View File

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

View File

@@ -1,5 +1,3 @@
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { LOCALSTORAGE } from 'constants/localStorage';
import getLabelName from 'lib/getLabelName';
import { QueryData } from 'types/api/widgets/getQuery';
@@ -102,7 +100,7 @@ export const saveLegendEntriesToLocalStorage = ({
try {
existingEntries = JSON.parse(
getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) || '[]',
localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) || '[]',
);
} catch (error) {
console.error('Error parsing LEGEND_GRAPH from local storage', error);
@@ -117,7 +115,7 @@ export const saveLegendEntriesToLocalStorage = ({
}
try {
setLocalStorageKey(
localStorage.setItem(
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
JSON.stringify(existingEntries),
);

View File

@@ -1,6 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
import type { NotificationInstance } from 'antd/es/notification/interface';
import getLocalStorageKey from 'api/browser/localstorage/get';
import { NavigateToExplorerProps } from 'components/CeleryTask/useNavigateToExplorer';
import { LOCALSTORAGE } from 'constants/localStorage';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -45,8 +44,8 @@ export const getLocalStorageGraphVisibilityState = ({
],
};
if (getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
const legendGraphFromLocalStore = getLocalStorageKey(
if (localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
const legendGraphFromLocalStore = localStorage.getItem(
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
);
let legendFromLocalStore: {
@@ -95,8 +94,8 @@ export const getGraphVisibilityStateOnDataChange = ({
graphVisibilityStates: Array(options.series.length).fill(true),
legendEntry: showAllDataSet(options),
};
if (getLocalStorageKey(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
const legendGraphFromLocalStore = getLocalStorageKey(
if (localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
const legendGraphFromLocalStore = localStorage.getItem(
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
);
let legendFromLocalStore: {

View File

@@ -10,7 +10,6 @@ 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 { openInNewTab } from 'utils/navigation';
import dialsUrl from '@/assets/Icons/dials.svg';
@@ -115,7 +114,7 @@ export default function Dashboards({
dashboardName: dashboard.data.title,
});
if (event.metaKey || event.ctrlKey) {
openInNewTab(getLink());
window.open(getLink(), '_blank');
} else {
safeNavigate(getLink());
}

View File

@@ -9,7 +9,6 @@ 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 { openInNewTab } from 'utils/navigation';
import containerPlusUrl from '@/assets/Icons/container-plus.svg';
import helloWaveUrl from '@/assets/Icons/hello-wave.svg';
@@ -52,7 +51,7 @@ function DataSourceInfo({
if (activeLicense && activeLicense.platform === LicensePlatform.CLOUD) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
openInNewTab(DOCS_LINKS.ADD_DATA_SOURCE);
window?.open(DOCS_LINKS.ADD_DATA_SOURCE, '_blank', 'noopener noreferrer');
}
};

View File

@@ -8,7 +8,6 @@ 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 { openInNewTab } from 'utils/navigation';
import './HomeChecklist.styles.scss';
@@ -100,7 +99,11 @@ function HomeChecklist({
) {
history.push(item.toRoute || '');
} else {
openInNewTab(item.docsLink || '');
window?.open(
item.docsLink || '',
'_blank',
'noopener noreferrer',
);
}
}}
>
@@ -116,7 +119,7 @@ function HomeChecklist({
step: item.id,
});
openInNewTab(item.docsLink ?? '');
window?.open(item.docsLink, '_blank', 'noopener noreferrer');
}}
>
<BookOpenText size={16} />

View File

@@ -31,7 +31,6 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { USER_ROLES } from 'types/roles';
import { isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import triangleRulerUrl from '@/assets/Icons/triangle-ruler.svg';
@@ -80,7 +79,11 @@ const EmptyState = memo(
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
openInNewTab(DOCS_LINKS.ADD_DATA_SOURCE);
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',
);
}
}}
>

View File

@@ -17,7 +17,6 @@ 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 { openInNewTab } from 'utils/navigation';
import triangleRulerUrl from '@/assets/Icons/triangle-ruler.svg';
@@ -134,7 +133,11 @@ export default function ServiceTraces({
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
openInNewTab(DOCS_LINKS.ADD_DATA_SOURCE);
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',
);
}
}}
>

View File

@@ -51,7 +51,6 @@ import {
LogsAggregatorOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import { filterDuplicateFilters } from '../commonUtils';
@@ -570,7 +569,10 @@ function K8sBaseDetails<T>({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -589,7 +591,10 @@ function K8sBaseDetails<T>({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
}
};

View File

@@ -4,7 +4,6 @@ import {
CloudintegrationtypesServiceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { withBasePath } from 'utils/basePath';
import './ServiceDashboards.styles.scss';
@@ -45,11 +44,7 @@ function ServiceDashboards({
return;
}
if (event.metaKey || event.ctrlKey) {
window.open(
withBasePath(dashboardUrl),
'_blank',
'noopener,noreferrer',
);
window.open(dashboardUrl, '_blank', 'noopener,noreferrer');
return;
}
safeNavigate(dashboardUrl);
@@ -59,11 +54,7 @@ function ServiceDashboards({
return;
}
if (event.button === 1) {
window.open(
withBasePath(dashboardUrl),
'_blank',
'noopener,noreferrer',
);
window.open(dashboardUrl, '_blank', 'noopener,noreferrer');
}
}}
onKeyDown={(event): void => {

View File

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

View File

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

View File

@@ -28,8 +28,6 @@ import {
Typography,
} from 'antd';
import type { TableProps } from 'antd/lib';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import createDashboard from 'api/v1/dashboards/create';
import { AxiosError } from 'axios';
@@ -85,8 +83,6 @@ import {
} from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { isModifierKeyPressed } from 'utils/app';
import { getAbsoluteUrl } from 'utils/basePath';
import { openInNewTab } from 'utils/navigation';
import awwSnapUrl from '@/assets/Icons/awwSnap.svg';
import dashboardsUrl from '@/assets/Icons/dashboards.svg';
@@ -149,7 +145,7 @@ function DashboardsList(): JSX.Element {
);
const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => {
const dashboardDynamicColumnsString = getLocalStorageKey('dashboard');
const dashboardDynamicColumnsString = localStorage.getItem('dashboard');
let dashboardDynamicColumns: DashboardDynamicColumns = {
createdAt: true,
createdBy: true,
@@ -163,7 +159,7 @@ function DashboardsList(): JSX.Element {
);
if (isEmpty(tempDashboardDynamicColumns)) {
setLocalStorageKey('dashboard', JSON.stringify(dashboardDynamicColumns));
localStorage.setItem('dashboard', JSON.stringify(dashboardDynamicColumns));
} else {
dashboardDynamicColumns = { ...tempDashboardDynamicColumns };
}
@@ -171,7 +167,7 @@ function DashboardsList(): JSX.Element {
console.error(error);
}
} else {
setLocalStorageKey('dashboard', JSON.stringify(dashboardDynamicColumns));
localStorage.setItem('dashboard', JSON.stringify(dashboardDynamicColumns));
}
return dashboardDynamicColumns;
@@ -185,7 +181,7 @@ function DashboardsList(): JSX.Element {
visibleColumns: DashboardDynamicColumns,
): void {
try {
setLocalStorageKey('dashboard', JSON.stringify(visibleColumns));
localStorage.setItem('dashboard', JSON.stringify(visibleColumns));
} catch (error) {
console.error(error);
}
@@ -461,7 +457,7 @@ function DashboardsList(): JSX.Element {
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
openInNewTab(getLink());
window.open(getLink(), '_blank');
}}
>
Open in New Tab
@@ -473,7 +469,7 @@ function DashboardsList(): JSX.Element {
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
setCopy(getAbsoluteUrl(getLink()));
setCopy(`${window.location.origin}${getLink()}`);
}}
>
Copy Link

View File

@@ -1,7 +1,6 @@
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';
@@ -13,7 +12,7 @@ function Name(name: Data['name'], data: Data): JSX.Element {
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
if (event.metaKey || event.ctrlKey) {
openInNewTab(getLink());
window.open(getLink(), '_blank');
} else {
history.push(getLink());
}

View File

@@ -17,7 +17,6 @@ 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 { withBasePath } from 'utils/basePath';
import { useContextLogData } from './useContextLogData';
@@ -117,7 +116,7 @@ function ContextLogRenderer({
);
const link = `${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`;
window.open(withBasePath(link), '_blank', 'noopener,noreferrer');
window.open(link, '_blank', 'noopener,noreferrer');
},
[query, urlQuery],
);

View File

@@ -34,7 +34,6 @@ 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';
@@ -192,7 +191,7 @@ function TableView({
if (event.ctrlKey || event.metaKey) {
// open the trace in new tab
openInNewTab(route);
window.open(route, '_blank');
} else {
history.push(route);
}

View File

@@ -213,7 +213,6 @@ function Login(): JSX.Element {
if (isCallbackAuthN) {
const url = form.getFieldValue('url');
// eslint-disable-next-line rulesdir/no-raw-absolute-path
window.location.href = url;
}
} catch (error) {

View File

@@ -34,7 +34,6 @@ import ROUTES from 'constants/routes';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDragColumns from 'hooks/useDragColumns';
import { getAbsoluteUrl } from 'utils/basePath';
import { infinityDefaultStyles } from '../InfinityTableView/config';
import { TanStackTableStyled } from '../InfinityTableView/styles';
@@ -240,7 +239,7 @@ const TanStackTableView = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
urlQuery.delete(QueryParams.activeLogId);
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.activeLogId, `"${logId}"`);
const link = getAbsoluteUrl(`${pathname}?${urlQuery.toString()}`);
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
setCopy(link);
toast.success('Copied to clipboard', { position: 'top-right' });

View File

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

View File

@@ -9,7 +9,6 @@ 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';
@@ -68,8 +67,9 @@ function DashboardsAndAlertsPopover({
<Typography.Link
key={alert.alertId}
onClick={(): void => {
openInNewTab(
window.open(
`${ROUTES.ALERT_OVERVIEW}?${QueryParams.ruleId}=${alert.alertId}`,
'_blank',
);
}}
className="dashboards-popover-content-item"
@@ -90,10 +90,11 @@ function DashboardsAndAlertsPopover({
<Typography.Link
key={dashboard.dashboardId}
onClick={(): void => {
openInNewTab(
window.open(
generatePath(ROUTES.DASHBOARD, {
dashboardId: dashboard.dashboardId,
}),
'_blank',
);
}}
className="dashboards-popover-content-item"

View File

@@ -18,7 +18,6 @@ import {
import useContextVariables from 'hooks/dashboard/useContextVariables';
import { Plus, Trash2 } from 'lucide-react';
import { ContextLinkProps, Widgets } from 'types/api/dashboard/getAll';
import { getBaseUrl } from 'utils/basePath';
import VariablesDropdown from './VariablesDropdown';
@@ -85,7 +84,7 @@ function UpdateContextLinks({
);
// Function to get current domain
const getCurrentDomain = (): string => getBaseUrl();
const getCurrentDomain = (): string => window.location.origin;
// Function to handle variable selection from dropdown
const handleVariableSelect = (

View File

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

View File

@@ -18,7 +18,6 @@ import {
Trash2,
} from 'lucide-react';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
import { v4 as uuid } from 'uuid';
import { OnboardingQuestionHeader } from '../OnboardingQuestionHeader';
@@ -61,7 +60,7 @@ function InviteTeamMembers({
email: '',
role: '',
name: '',
frontendBaseUrl: getBaseUrl(),
frontendBaseUrl: window.location.origin,
id: '',
};

View File

@@ -8,7 +8,6 @@ import { DOCS_BASE_URL } from 'constants/app';
import { useGetGlobalConfig } from 'hooks/globalConfig/useGetGlobalConfig';
import { useNotifications } from 'hooks/useNotifications';
import { ArrowUpRight, Copy, Info, Key, TriangleAlert } from 'lucide-react';
import { withBasePath } from 'utils/basePath';
import './IngestionDetails.styles.scss';
@@ -216,7 +215,7 @@ export default function OnboardingIngestionDetails(): JSX.Element {
</a>
. To create a new one,{' '}
<a
href={withBasePath('/settings/ingestion-settings')}
href="/settings/ingestion-settings"
target="_blank"
className="learn-more"
rel="noreferrer"

View File

@@ -8,7 +8,6 @@ import { useNotifications } from 'hooks/useNotifications';
import { cloneDeep, debounce, isEmpty } from 'lodash-es';
import { ArrowRight, CheckCircle, Plus, TriangleAlert, X } from 'lucide-react';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
import { v4 as uuid } from 'uuid';
import './InviteTeamMembers.styles.scss';
@@ -57,7 +56,7 @@ function InviteTeamMembers({
email: '',
role: 'EDITOR',
name: '',
frontendBaseUrl: getBaseUrl(),
frontendBaseUrl: window.location.origin,
id: '',
};

View File

@@ -18,7 +18,6 @@ import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import CopyToClipboard from 'periscope/components/CopyToClipboard';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { getAbsoluteUrl } from 'utils/basePath';
import CreateEdit from './CreateEdit/CreateEdit';
import SSOEnforcementToggle from './SSOEnforcementToggle';
@@ -146,7 +145,7 @@ function AuthDomain(): JSX.Element {
return <span className="auth-domain-list-na">N/A</span>;
}
const href = getAbsoluteUrl(`/${relayPath}`);
const href = `${window.location.origin}/${relayPath}`;
return <CopyToClipboard textToCopy={href} />;
},
},

View File

@@ -4,7 +4,6 @@ import { Button, Form, FormInstance, Modal } from 'antd';
import sendInvite from 'api/v1/invite/create';
import { useNotifications } from 'hooks/useNotifications';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
import InviteTeamMembers from '../InviteTeamMembers';
import { InviteMemberFormValues } from '../utils';
@@ -41,7 +40,7 @@ function InviteUserModal(props: InviteUserModalProps): JSX.Element {
email: member.email,
name: member?.name,
role: member.role,
frontendBaseUrl: getBaseUrl(),
frontendBaseUrl: window.location.origin,
});
notifications.success({

View File

@@ -14,7 +14,6 @@ import ContextMenu from 'periscope/components/ContextMenu';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { ContextLinksData } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { openInNewTab } from 'utils/navigation';
import { ContextMenuItem } from './contextConfig';
import { getDataLinks } from './dataLinksUtils';
@@ -116,7 +115,7 @@ const useBaseAggregateOptions = ({
key={id}
icon={<LinkOutlined />}
onClick={(): void => {
openInNewTab(url);
window.open(url, '_blank');
onClose?.();
}}
>

View File

@@ -14,7 +14,6 @@ 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 {
@@ -77,7 +76,7 @@ function RoutingPolicyDetails({
style={{ padding: '0 4px' }}
type="link"
onClick={(): void => {
openInNewTab(ROUTES.CHANNELS_NEW);
window.open(ROUTES.CHANNELS_NEW, '_blank');
}}
>
here.

View File

@@ -818,7 +818,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
);
if (item && !('type' in item) && item.isExternal && item.url) {
openInNewTab(item.url);
window.open(item.url, '_blank');
}
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;

View File

@@ -28,7 +28,6 @@ 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';
@@ -144,7 +143,7 @@ function SpanLogs({
const url = `${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`;
openInNewTab(url);
window.open(url, '_blank');
},
[
isLogSpanRelated,

View File

@@ -17,7 +17,6 @@ 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 { getAbsoluteUrl } from 'utils/basePath';
import { RelatedSignalsViews } from '../constants';
import SpanLogs from '../SpanLogs/SpanLogs';
@@ -159,7 +158,9 @@ function SpanRelatedSignals({
searchParams.set(QueryParams.endTime, endTimeMs.toString());
window.open(
getAbsoluteUrl(`${ROUTES.LOGS_EXPLORER}?${searchParams.toString()}`),
`${window.location.origin}${
ROUTES.LOGS_EXPLORER
}?${searchParams.toString()}`,
'_blank',
'noopener,noreferrer',
);

View File

@@ -31,7 +31,6 @@ 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);
@@ -215,7 +214,7 @@ function TraceTable(): JSX.Element {
event.preventDefault();
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
openInNewTab(getLink(record));
window.open(getLink(record), '_blank');
} else {
history.push(getLink(record));
}

View File

@@ -28,7 +28,6 @@ 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';
@@ -87,7 +86,7 @@ function TracesTableComponent({
event.preventDefault();
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
openInNewTab(getTraceLink(record));
window.open(getTraceLink(record), '_blank');
} else {
history.push(getTraceLink(record));
}

View File

@@ -1,5 +1,4 @@
import { useState } from 'react';
import setSessionStorageApi from 'api/browser/sessionstorage/set';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import isEqual from 'lodash-es/isEqual';
@@ -62,7 +61,7 @@ function useDashboardsListQueryParams(): {
const queryParamsString = params.toString();
setSessionStorageApi(
sessionStorage.setItem(
DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY,
queryParamsString,
);

View File

@@ -172,7 +172,6 @@ export function useIntegrationModal({
id: accountId,
},
);
// eslint-disable-next-line rulesdir/no-raw-absolute-path -- connectionUrl is an external AWS console URL, not an internal path
window.open(connectionUrl, '_blank');
setModalState(ModalStateEnum.WAITING);
setAccountId(accountId);

View File

@@ -17,7 +17,6 @@ import useUrlQuery from 'hooks/useUrlQuery';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getAbsoluteUrl } from 'utils/basePath';
import { HIGHLIGHTED_DELAY } from './configs';
import { UseCopyLogLink } from './types';
@@ -61,7 +60,7 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
urlQuery.set(QueryParams.startTime, minTime?.toString() || '');
urlQuery.set(QueryParams.endTime, maxTime?.toString() || '');
const link = getAbsoluteUrl(`${pathname}?${urlQuery.toString()}`);
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
setCopy(link);

View File

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

View File

@@ -4,7 +4,6 @@ import { useCopyToClipboard } from 'react-use';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { Span } from 'types/api/trace/getTraceV2';
import { getAbsoluteUrl } from 'utils/basePath';
export const useCopySpanLink = (
span?: Span,
@@ -29,7 +28,7 @@ export const useCopySpanLink = (
urlQuery.set('spanId', span?.spanId);
}
const link = getAbsoluteUrl(`${pathname}?${urlQuery.toString()}`);
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
setCopy(link);
notifications.success({

View File

@@ -1,20 +1,34 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import getLocalStorageApi from 'api/browser/localstorage/get';
import removeLocalStorageApi from 'api/browser/localstorage/remove';
import setLocalStorageApi from 'api/browser/localstorage/set';
/**
* A React hook for interacting with localStorage.
* It allows getting, setting, and removing items from localStorage.
*
* @template T The type of the value to be stored.
* @param {string} key The localStorage key.
* @param {T | (() => T)} defaultValue The default value to use if no value is found in localStorage,
* @returns {[T, (value: T | ((prevState: T) => T)) => void, () => void]}
* A tuple containing:
* - The current value from state (and localStorage).
* - A function to set the value (updates state and localStorage).
* - A function to remove the value from localStorage and reset state to defaultValue.
*/
export function useLocalStorage<T>(
key: string,
defaultValue: T | (() => T),
): [T, (value: T | ((prevState: T) => T)) => void, () => void] {
// Stabilize the defaultValue to prevent unnecessary re-renders
const defaultValueRef = useRef<T | (() => T)>(defaultValue);
// Update the ref if defaultValue changes (for cases where it's intentionally dynamic)
useEffect(() => {
if (defaultValueRef.current !== defaultValue) {
defaultValueRef.current = defaultValue;
}
}, [defaultValue]);
// This function resolves the defaultValue if it's a function,
// and handles potential errors during localStorage access or JSON parsing.
const readValueFromStorage = useCallback((): T => {
const resolveddefaultValue =
defaultValueRef.current instanceof Function
@@ -22,25 +36,33 @@ export function useLocalStorage<T>(
: defaultValueRef.current;
try {
const item = getLocalStorageApi(key);
const item = window.localStorage.getItem(key);
// If item exists, parse it, otherwise return the resolved default value.
if (item) {
return JSON.parse(item) as T;
}
} catch (error) {
// Log error and fall back to default value if reading/parsing fails.
console.warn(`Error reading localStorage key "${key}":`, error);
}
return resolveddefaultValue;
}, [key]);
// Initialize state by reading from localStorage.
const [storedValue, setStoredValue] = useState<T>(readValueFromStorage);
// This function updates both localStorage and the React state.
const setValue = useCallback(
(value: T | ((prevState: T) => T)) => {
try {
// If a function is passed to setValue, it receives the latest value from storage.
const latestValueFromStorage = readValueFromStorage();
const valueToStore =
value instanceof Function ? value(latestValueFromStorage) : value;
setLocalStorageApi(key, JSON.stringify(valueToStore));
// Save to localStorage.
window.localStorage.setItem(key, JSON.stringify(valueToStore));
// Update React state.
setStoredValue(valueToStore);
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error);
@@ -49,9 +71,11 @@ export function useLocalStorage<T>(
[key, readValueFromStorage],
);
// This function removes the item from localStorage and resets the React state.
const removeValue = useCallback(() => {
try {
removeLocalStorageApi(key);
window.localStorage.removeItem(key);
// Reset state to the (potentially resolved) defaultValue.
setStoredValue(
defaultValueRef.current instanceof Function
? (defaultValueRef.current as () => T)()
@@ -62,9 +86,12 @@ export function useLocalStorage<T>(
}
}, [key]);
// useEffect to update the storedValue if the key changes,
// or if the defaultValue prop changes causing readValueFromStorage to change.
// This ensures the hook reflects the correct localStorage item if its key prop dynamically changes.
useEffect(() => {
setStoredValue(readValueFromStorage());
}, [key, readValueFromStorage]);
}, [key, readValueFromStorage]); // Re-run if key or the read function changes.
return [storedValue, setValue, removeValue];
}

View File

@@ -108,18 +108,19 @@ export const useSafeNavigate = (
const safeNavigate = useCallback(
// eslint-disable-next-line sonarjs/cognitive-complexity
(to: string | SafeNavigateParams, options?: NavigateOptions) => {
// eslint-disable-next-line rulesdir/no-raw-absolute-path
const base = window.location.origin;
const currentUrl = new URL(`${location.pathname}${location.search}`, base);
const currentUrl = new URL(
`${location.pathname}${location.search}`,
window.location.origin,
);
let targetUrl: URL;
if (typeof to === 'string') {
targetUrl = new URL(to, base);
targetUrl = new URL(to, window.location.origin);
} else {
targetUrl = new URL(
`${to.pathname || location.pathname}${to.search || ''}`,
base,
window.location.origin,
);
}

View File

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

View File

@@ -59,7 +59,7 @@ function MessagingQueues(): JSX.Element {
history.push(link);
}
} else {
openInNewTab(KAFKA_SETUP_DOC_LINK);
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
}
};

View File

@@ -20,8 +20,6 @@ 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 { getBaseUrl } from 'utils/basePath';
import { openInNewTab } from 'utils/navigation';
import './Support.styles.scss';
@@ -94,7 +92,7 @@ export default function Support(): JSX.Element {
const { pathname } = useLocation();
const handleChannelWithRedirects = (url: string): void => {
openInNewTab(url);
window.open(url, '_blank');
};
useEffect(() => {
@@ -152,7 +150,7 @@ export default function Support(): JSX.Element {
});
updateCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
};

View File

@@ -29,7 +29,6 @@ import { useAppContext } from 'providers/App/App';
import APIError from 'types/api/error';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { isModifierKeyPressed } from 'utils/app';
import { getBaseUrl } from 'utils/basePath';
import { getFormattedDate } from 'utils/timeUtils';
import CustomerStoryCard from './CustomerStoryCard';
@@ -116,7 +115,7 @@ export default function WorkspaceBlocked(): JSX.Element {
logEvent('Workspace Blocked: User Clicked Update Credit Card', {});
updateCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
}, [updateCreditCard]);

View File

@@ -21,7 +21,6 @@ import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import APIError from 'types/api/error';
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
import { getBaseUrl } from 'utils/basePath';
import { getFormattedDateWithMinutes } from 'utils/timeUtils';
import featureGraphicCorrelationUrl from '@/assets/Images/feature-graphic-correlation.svg';
@@ -58,7 +57,7 @@ function WorkspaceSuspended(): JSX.Element {
const handleUpdateCreditCard = useCallback(async () => {
manageCreditCard({
url: getBaseUrl(),
url: window.location.origin,
});
}, [manageCreditCard]);

View File

@@ -1,8 +1,3 @@
import getSessionStorageApi from 'api/browser/sessionstorage/get';
import removeSessionStorageApi from 'api/browser/sessionstorage/remove';
import setSessionStorageApi from 'api/browser/sessionstorage/set';
import { getScopedKey } from 'utils/storage';
const PREFIX = 'dashboard_row_widget_';
function getKey(dashboardId: string): string {
@@ -13,25 +8,21 @@ export function setSelectedRowWidgetId(
dashboardId: string,
widgetId: string,
): void {
const unscopedKey = getKey(dashboardId);
const scopedPrefix = getScopedKey(PREFIX);
const scopedKey = getScopedKey(unscopedKey);
const key = getKey(dashboardId);
// Object.keys returns the raw/already-scoped keys from the browser.
// Direct sessionStorage.removeItem is intentional here — k is already fully scoped.
// eslint-disable-next-line no-restricted-globals
// remove all other selected widget ids for the dashboard before setting the new one
// to ensure only one widget is selected at a time. Helps out in weird navigate and refresh scenarios
Object.keys(sessionStorage)
.filter((k) => k.startsWith(scopedPrefix) && k !== scopedKey)
// eslint-disable-next-line no-restricted-globals
.filter((k) => k.startsWith(PREFIX) && k !== key)
.forEach((k) => sessionStorage.removeItem(k));
setSessionStorageApi(unscopedKey, widgetId);
sessionStorage.setItem(key, widgetId);
}
export function getSelectedRowWidgetId(dashboardId: string): string | null {
return getSessionStorageApi(getKey(dashboardId));
return sessionStorage.getItem(getKey(dashboardId));
}
export function clearSelectedRowWidgetId(dashboardId: string): void {
removeSessionStorageApi(getKey(dashboardId));
sessionStorage.removeItem(getKey(dashboardId));
}

View File

@@ -22,7 +22,6 @@ import { LOCALSTORAGE } from 'constants/localStorage';
import { EventListener, EventSourcePolyfill } from 'event-source-polyfill';
import { useNotifications } from 'hooks/useNotifications';
import APIError from 'types/api/error';
import { withBasePath } from 'utils/basePath';
interface IEventSourceContext {
eventSourceInstance: EventSourcePolyfill | null;
@@ -130,12 +129,9 @@ export function EventSourceProvider({
const handleStartOpenConnection = useCallback(
(filterExpression?: string): void => {
const apiPath = `${apiV3}logs/livetail?filter=${encodeURIComponent(
filterExpression || '',
)}`;
const eventSourceUrl = ENVIRONMENT.baseURL
? `${ENVIRONMENT.baseURL}${apiPath}`
: withBasePath(apiPath);
const eventSourceUrl = `${
ENVIRONMENT.baseURL
}${apiV3}logs/livetail?filter=${encodeURIComponent(filterExpression || '')}`;
eventSourceRef.current = new EventSourcePolyfill(eventSourceUrl, {
headers: {

View File

@@ -9,8 +9,6 @@ import React, {
useMemo,
useState,
} from 'react';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import {
getBrowserTimezone,
getTimezoneObjectByTimezoneString,
@@ -45,7 +43,7 @@ function TimezoneProvider({
}): JSX.Element {
const getStoredTimezoneValue = (): Timezone | null => {
try {
const timezoneValue = getLocalStorageKey(LOCALSTORAGE.PREFERRED_TIMEZONE);
const timezoneValue = localStorage.getItem(LOCALSTORAGE.PREFERRED_TIMEZONE);
if (timezoneValue) {
return getTimezoneObjectByTimezoneString(timezoneValue);
}
@@ -57,7 +55,7 @@ function TimezoneProvider({
const setStoredTimezoneValue = (value: string): void => {
try {
setLocalStorageKey(LOCALSTORAGE.PREFERRED_TIMEZONE, value);
localStorage.setItem(LOCALSTORAGE.PREFERRED_TIMEZONE, value);
} catch (error) {
console.error('Error saving timezone to localStorage:', error);
}

View File

@@ -1,5 +1,4 @@
import { Dispatch, SetStateAction } from 'react';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { TelemetryFieldKey } from 'api/v5/v5';
import { LOCALSTORAGE } from 'constants/localStorage';
@@ -49,7 +48,7 @@ const getLogsUpdaterConfig = (
// Also update local storage
const local = JSON.parse(
getLocalStorageKey(LOCALSTORAGE.LOGS_LIST_OPTIONS) || '{}',
localStorage.getItem(LOCALSTORAGE.LOGS_LIST_OPTIONS) || '{}',
);
local.selectColumns = newColumns;
setLocalStorageKey(LOCALSTORAGE.LOGS_LIST_OPTIONS, JSON.stringify(local));
@@ -77,7 +76,7 @@ const getLogsUpdaterConfig = (
// Also update local storage
const local = JSON.parse(
getLocalStorageKey(LOCALSTORAGE.LOGS_LIST_OPTIONS) || '{}',
localStorage.getItem(LOCALSTORAGE.LOGS_LIST_OPTIONS) || '{}',
);
Object.assign(local, newFormatting);
setLocalStorageKey(LOCALSTORAGE.LOGS_LIST_OPTIONS, JSON.stringify(local));

View File

@@ -1,5 +1,4 @@
import { Dispatch, SetStateAction } from 'react';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { TelemetryFieldKey } from 'api/v5/v5';
import { LOCALSTORAGE } from 'constants/localStorage';
@@ -38,7 +37,7 @@ const getTracesUpdaterConfig = (
});
const local = JSON.parse(
getLocalStorageKey(LOCALSTORAGE.TRACES_LIST_OPTIONS) || '{}',
localStorage.getItem(LOCALSTORAGE.TRACES_LIST_OPTIONS) || '{}',
);
local.selectColumns = newColumns;
setLocalStorageKey(LOCALSTORAGE.TRACES_LIST_OPTIONS, JSON.stringify(local));

View File

@@ -70,7 +70,7 @@ export const updateURL = (
userSelectedFilter: JSON.stringify(Object.fromEntries(userSelectedFilter)),
};
history.replace(
`${history.location.pathname}?${createQueryParams(queryParams)}`,
`${window.location.pathname}?${createQueryParams(queryParams)}`,
);
};

View File

@@ -29,7 +29,6 @@ import {
UPDATE_SELECTED_FIELDS,
} from 'types/actions/logs';
import { ILogsReducer } from 'types/reducer/logs';
import { withBasePath } from 'utils/basePath';
const supportedLogsOrder = [
OrderPreferenceItems.ASC,
@@ -38,7 +37,7 @@ const supportedLogsOrder = [
function getLogsOrder(): OrderPreferenceItems {
// set the value of order from the URL only when order query param is present and the user is landing on the old logs explorer page
if (window.location.pathname === withBasePath(ROUTES.OLD_LOGS_EXPLORER)) {
if (window.location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
const orderParam = new URLSearchParams(window.location.search).get('order');
if (orderParam) {

View File

@@ -1,72 +0,0 @@
/**
* storage.ts memoizes basePath at module init (via basePath.ts IIFE).
* Use jest.isolateModules to re-import storage with a fresh DOM state each time.
*/
type StorageModule = typeof import('../storage');
function loadStorageModule(href?: string): StorageModule {
if (href !== undefined) {
const base = document.createElement('base');
base.setAttribute('href', href);
document.head.appendChild(base);
}
let mod!: StorageModule;
jest.isolateModules(() => {
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
mod = require('../storage');
});
return mod;
}
afterEach(() => {
document.head.querySelectorAll('base').forEach((el) => el.remove());
localStorage.clear();
});
describe('getScopedKey — root path "/"', () => {
it('returns the bare key unchanged', () => {
const { getScopedKey } = loadStorageModule('/');
expect(getScopedKey('AUTH_TOKEN')).toBe('AUTH_TOKEN');
});
it('backward compat: scoped key equals direct localStorage key', () => {
const { getScopedKey } = loadStorageModule('/');
localStorage.setItem('AUTH_TOKEN', 'tok');
expect(localStorage.getItem(getScopedKey('AUTH_TOKEN'))).toBe('tok');
});
});
describe('getScopedKey — prefixed path "/signoz/"', () => {
it('prefixes the key with the base path', () => {
const { getScopedKey } = loadStorageModule('/signoz/');
expect(getScopedKey('AUTH_TOKEN')).toBe('/signoz/AUTH_TOKEN');
});
it('isolates from root namespace', () => {
const { getScopedKey } = loadStorageModule('/signoz/');
localStorage.setItem('AUTH_TOKEN', 'root-tok');
expect(localStorage.getItem(getScopedKey('AUTH_TOKEN'))).toBeNull();
});
});
describe('getScopedKey — prefixed path "/testing/"', () => {
it('prefixes the key with /testing/', () => {
const { getScopedKey } = loadStorageModule('/testing/');
expect(getScopedKey('THEME')).toBe('/testing/THEME');
});
});
describe('getScopedKey — prefixed path "/playwright/"', () => {
it('prefixes the key with /playwright/', () => {
const { getScopedKey } = loadStorageModule('/playwright/');
expect(getScopedKey('THEME')).toBe('/playwright/THEME');
});
});
describe('getScopedKey — no <base> tag', () => {
it('falls back to bare key (basePath defaults to "/")', () => {
const { getScopedKey } = loadStorageModule();
expect(getScopedKey('THEME')).toBe('THEME');
});
});

View File

@@ -35,7 +35,6 @@ export function withBasePath(path: string): string {
* getAbsoluteUrl(ROUTES.LOGS_EXPLORER) → 'https://host/signoz/logs/logs-explorer'
*/
export function getAbsoluteUrl(path: string): string {
// eslint-disable-next-line rulesdir/no-raw-absolute-path
return window.location.origin + withBasePath(path);
}
@@ -45,7 +44,7 @@ export function getAbsoluteUrl(path: string): string {
* getBaseUrl() → 'https://host/signoz'
*/
export function getBaseUrl(): string {
// eslint-disable-next-line rulesdir/no-raw-absolute-path
const origin = window.location.origin;
return origin + (_basePath === '/' ? '' : _basePath.slice(0, -1));
return (
window.location.origin + (_basePath === '/' ? '' : _basePath.slice(0, -1))
);
}

View File

@@ -1,6 +1,3 @@
import getLocalStorageKey from 'api/browser/localstorage/get';
import removeLocalStorageKey from 'api/browser/localstorage/remove';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { LOCALSTORAGE } from 'constants/localStorage';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import dayjs from 'dayjs';
@@ -19,7 +16,9 @@ const MAX_STORED_RANGES = 3;
*/
export const getCustomTimeRanges = (): CustomTimeRange[] => {
try {
const stored = getLocalStorageKey(LOCALSTORAGE.LAST_USED_CUSTOM_TIME_RANGES);
const stored = localStorage.getItem(
LOCALSTORAGE.LAST_USED_CUSTOM_TIME_RANGES,
);
if (!stored) {
return [];
}
@@ -79,7 +78,7 @@ export const addCustomTimeRange = (
// Store in localStorage
try {
setLocalStorageKey(
localStorage.setItem(
LOCALSTORAGE.LAST_USED_CUSTOM_TIME_RANGES,
JSON.stringify(updatedRanges),
);
@@ -95,7 +94,7 @@ export const addCustomTimeRange = (
*/
export const clearCustomTimeRanges = (): void => {
try {
removeLocalStorageKey(LOCALSTORAGE.LAST_USED_CUSTOM_TIME_RANGES);
localStorage.removeItem(LOCALSTORAGE.LAST_USED_CUSTOM_TIME_RANGES);
} catch (error) {
console.warn('Failed to clear custom time ranges from localStorage:', error);
}
@@ -113,7 +112,7 @@ export const removeCustomTimeRange = (timestamp: number): CustomTimeRange[] => {
);
try {
setLocalStorageKey(
localStorage.setItem(
LOCALSTORAGE.LAST_USED_CUSTOM_TIME_RANGES,
JSON.stringify(updatedRanges),
);

View File

@@ -1,5 +1,3 @@
import getSessionStorageApi from 'api/browser/sessionstorage/get';
import setSessionStorageApi from 'api/browser/sessionstorage/set';
import { SESSIONSTORAGE } from 'constants/sessionStorage';
type ComponentImport = () => Promise<any>;
@@ -7,17 +5,18 @@ type ComponentImport = () => Promise<any>;
export const lazyRetry = (componentImport: ComponentImport): Promise<any> =>
new Promise((resolve, reject) => {
const hasRefreshed: boolean = JSON.parse(
getSessionStorageApi(SESSIONSTORAGE.RETRY_LAZY_REFRESHED) || 'false',
window.sessionStorage.getItem(SESSIONSTORAGE.RETRY_LAZY_REFRESHED) ||
'false',
);
componentImport()
.then((component: any) => {
setSessionStorageApi(SESSIONSTORAGE.RETRY_LAZY_REFRESHED, 'false');
window.sessionStorage.setItem(SESSIONSTORAGE.RETRY_LAZY_REFRESHED, 'false');
resolve(component);
})
.catch((error: Error) => {
if (!hasRefreshed) {
setSessionStorageApi(SESSIONSTORAGE.RETRY_LAZY_REFRESHED, 'true');
window.sessionStorage.setItem(SESSIONSTORAGE.RETRY_LAZY_REFRESHED, 'true');
window.location.reload();
}

View File

@@ -1,11 +0,0 @@
import { getBasePath } from 'utils/basePath';
/**
* Returns a storage key scoped to the runtime base path.
* At root ("/") the bare key is returned unchanged — backward compatible.
* At any other prefix the key is prefixed: "/signoz/AUTH_TOKEN".
*/
export function getScopedKey(key: string): string {
const basePath = getBasePath();
return basePath === '/' ? key : `${basePath}${key}`;
}

View File

@@ -207,16 +207,23 @@ func AdjustKey(key *telemetrytypes.TelemetryFieldKey, keys map[string][]*telemet
indexes := []telemetrytypes.JSONDataTypeIndex{}
fieldContextsSeen := map[telemetrytypes.FieldContext]bool{}
dataTypesSeen := map[telemetrytypes.FieldDataType]bool{}
jsonTypesSeen := map[string]*telemetrytypes.JSONDataType{}
for _, matchingKey := range matchingKeys {
materialized = materialized && matchingKey.Materialized
fieldContextsSeen[matchingKey.FieldContext] = true
dataTypesSeen[matchingKey.FieldDataType] = true
if matchingKey.JSONDataType != nil {
jsonTypesSeen[matchingKey.JSONDataType.StringValue()] = matchingKey.JSONDataType
}
indexes = append(indexes, matchingKey.Indexes...)
}
for _, matchingKey := range contextPrefixedMatchingKeys {
materialized = materialized && matchingKey.Materialized
fieldContextsSeen[matchingKey.FieldContext] = true
dataTypesSeen[matchingKey.FieldDataType] = true
if matchingKey.JSONDataType != nil {
jsonTypesSeen[matchingKey.JSONDataType.StringValue()] = matchingKey.JSONDataType
}
indexes = append(indexes, matchingKey.Indexes...)
}
key.Materialized = materialized
@@ -241,6 +248,15 @@ func AdjustKey(key *telemetrytypes.TelemetryFieldKey, keys map[string][]*telemet
break
}
}
if len(jsonTypesSeen) == 1 && key.JSONDataType == nil {
// all matching keys have same JSON data type, use it
for _, jt := range jsonTypesSeen {
actions = append(actions, fmt.Sprintf("Adjusting key %s to have JSON data type %s", key, jt.StringValue()))
key.JSONDataType = jt
break
}
}
}
return actions

View File

@@ -318,7 +318,7 @@ func TestVisitKey(t *testing.T) {
{
Name: "count",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeInt64,
FieldDataType: telemetrytypes.FieldDataTypeNumber,
},
},
},
@@ -326,7 +326,7 @@ func TestVisitKey(t *testing.T) {
{
Name: "count",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeInt64,
FieldDataType: telemetrytypes.FieldDataTypeNumber,
},
},
expectedErrors: nil,

View File

@@ -44,6 +44,7 @@ func (c *conditionBuilder) conditionFor(
}
return cond, nil
}
}
if operator.IsStringSearchOperator() {

View File

@@ -276,10 +276,14 @@ func (m *fieldMapper) FieldFor(ctx context.Context, tsStart, tsEnd uint64, key *
continue
}
if key.FieldDataType == telemetrytypes.FieldDataTypeUnspecified {
if key.JSONDataType == nil {
return "", qbtypes.ErrColumnNotFound
}
if key.KeyNameContainsArray() && !key.JSONDataType.IsArray {
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "FieldFor not supported for nested fields; only supported for flat paths (e.g. body.status.detail) and paths of Array type: %s(%s)", key.Name, key.FieldDataType)
}
expr, err := m.buildFieldForJSON(key)
if err != nil {
return "", err
@@ -350,6 +354,7 @@ func (m *fieldMapper) ColumnExpressionFor(
field *telemetrytypes.TelemetryFieldKey,
keys map[string][]*telemetrytypes.TelemetryFieldKey,
) (string, error) {
fieldExpression, err := m.FieldFor(ctx, tsStart, tsEnd, field)
if errors.Is(err, qbtypes.ErrColumnNotFound) {
// the key didn't have the right context to be added to the query
@@ -388,8 +393,6 @@ func (m *fieldMapper) ColumnExpressionFor(
}
fieldExpression = fmt.Sprintf("multiIf(%s, NULL)", strings.Join(args, ", "))
}
} else if err != nil {
return "", err
}
return fmt.Sprintf("%s AS `%s`", sqlbuilder.Escape(fieldExpression), field.Name), nil

View File

@@ -172,15 +172,30 @@ func (c *jsonConditionBuilder) terminalIndexedCondition(node *telemetrytypes.JSO
if strings.Contains(fieldPath, telemetrytypes.ArraySepSuffix) {
return "", errors.NewInternalf(CodeArrayNavigationFailed, "can not build index condition for array field %s", fieldPath)
}
if !node.IsTerminal {
return "", errors.NewInternalf(errors.CodeInvalidInput, "can not build index condition for non-terminal node %s", fieldPath)
elemType := node.TerminalConfig.ElemType
dynamicExpr := fmt.Sprintf("dynamicElement(%s, '%s')", fieldPath, elemType.StringValue())
indexedExpr := assumeNotNull(dynamicExpr)
// switch the operator and value for exists and not exists
switch operator {
case qbtypes.FilterOperatorExists:
operator = qbtypes.FilterOperatorNotEqual
value = getEmptyValue(elemType)
case qbtypes.FilterOperatorNotExists:
operator = qbtypes.FilterOperatorEqual
value = getEmptyValue(elemType)
default:
// do nothing
}
indexedExpr := schemamigrator.JSONSubColumnIndexExpr(node.Parent.Name, node.Name, node.TerminalConfig.ElemType.StringValue())
// TODO(Piyush): indexedExpr should not be formatted here instead value should be formatted
// else ClickHouse may not utilize index
indexedExpr, formattedValue := querybuilder.DataTypeCollisionHandledFieldName(node.TerminalConfig.Key, value, indexedExpr, operator)
return c.applyOperator(sb, indexedExpr, operator, formattedValue)
cond, err := c.applyOperator(sb, indexedExpr, operator, formattedValue)
if err != nil {
return "", err
}
return cond, nil
}
// buildPrimitiveTerminalCondition builds the condition if the terminal node is a primitive type
@@ -189,31 +204,32 @@ func (c *jsonConditionBuilder) buildPrimitiveTerminalCondition(node *telemetryty
fieldPath := node.FieldPath()
conditions := []string{}
// Utilize indexes when available, except for EXISTS/NOT EXISTS checks.
// Indexed columns always store a default empty value for absent fields (e.g. "" for strings,
// 0 for numbers), so using the index for existence checks would incorrectly exclude rows where
// the field genuinely holds the empty/zero value.
// utilize indexes for the condition if available
//
// Note: indexing is also skipped for Array Nested fields because they cannot be indexed.
// Note: Indexing code doesn't get executed for Array Nested fields because they can not be indexed
indexed := slices.ContainsFunc(node.TerminalConfig.Key.Indexes, func(index telemetrytypes.JSONDataTypeIndex) bool {
return index.Type == node.TerminalConfig.ElemType
})
isExistsCheck := operator == qbtypes.FilterOperatorExists || operator == qbtypes.FilterOperatorNotExists
if node.TerminalConfig.ElemType.IndexSupported && indexed && !isExistsCheck {
if node.TerminalConfig.ElemType.IndexSupported && indexed {
indexCond, err := c.terminalIndexedCondition(node, operator, value, sb)
if err != nil {
return "", err
}
// With a concrete non-zero value the index condition is self-contained.
// if qb has a definitive value, we can skip adding a condition to
// check the existence of the path in the json column
if value != nil && value != getEmptyValue(node.TerminalConfig.ElemType) {
return indexCond, nil
}
// The value is nil or the type's zero/empty value. Because indexed columns always store
// that zero value for absent fields, the index alone cannot distinguish "field is absent"
// from "field exists with zero value". Append a path-existence check (IS NOT NULL) as a
// second condition and AND them together.
conditions = append(conditions, indexCond)
operator = qbtypes.FilterOperatorExists
// Switch operator to EXISTS except when operator is NOT EXISTS since
// indexed paths on assumedNotNull, indexes will always have a default
// value so we flip the operator to Exists and filter the rows that
// actually have the value
if operator != qbtypes.FilterOperatorNotExists {
operator = qbtypes.FilterOperatorExists
}
}
var formattedValue = value
@@ -223,15 +239,20 @@ func (c *jsonConditionBuilder) buildPrimitiveTerminalCondition(node *telemetryty
fieldExpr := fmt.Sprintf("dynamicElement(%s, '%s')", fieldPath, node.TerminalConfig.ElemType.StringValue())
// For non-nested paths with a negative comparison operator (e.g. !=, NOT LIKE, NOT IN),
// wrap in assumeNotNull so ClickHouse treats absent paths as the zero value rather than NULL,
// which would otherwise cause them to be silently dropped from results.
// NOT EXISTS is excluded: we want a true NULL check there, not a zero-value stand-in.
// if operator is negative and has a value comparison i.e. excluding EXISTS and NOT EXISTS, we need to assume that the field exists everywhere
//
// Note: for nested array paths, buildAccessNodeBranches already inverts the operator before
// reaching here, so IsNonNestedPath() guards against double-applying the wrapping.
if node.IsNonNestedPath() && operator.IsNegativeOperator() && operator != qbtypes.FilterOperatorNotExists {
fieldExpr = assumeNotNull(fieldExpr)
// Note: here applyNotCondition will return true only if; top level path is being queried and operator is a negative operator
// Otherwise this code will be triggered by buildAccessNodeBranches; Where operator would've been already inverted if needed.
if node.IsNonNestedPath() {
yes, _ := applyNotCondition(operator)
if yes {
switch operator {
case qbtypes.FilterOperatorNotExists:
// skip
default:
fieldExpr = assumeNotNull(fieldExpr)
}
}
}
fieldExpr, formattedValue = querybuilder.DataTypeCollisionHandledFieldName(node.TerminalConfig.Key, formattedValue, fieldExpr, operator)

View File

@@ -220,7 +220,7 @@ func TestJSONStmtBuilder_PrimitivePaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((LOWER(toString(dynamicElement(body_v2.`user.age`, 'Int64'))) LIKE LOWER(?)) AND has(JSONAllPaths(body_v2), 'user.age')) OR ((LOWER(dynamicElement(body_v2.`user.age`, 'String')) LIKE LOWER(?)) AND has(JSONAllPaths(body_v2), 'user.age')))",
Args: []any{"%25%", "%25%", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `user.age` is ambiguous, found 2 different combinations of field context / data type: [name=user.age,context=body,datatype=int64 name=user.age,context=body,datatype=string]."},
Warnings: []string{"Key `user.age` is ambiguous, found 2 different combinations of field context / data type: [name=user.age,context=body,datatype=int64,jsondatatype=Int64 name=user.age,context=body,datatype=string,jsondatatype=String]."},
},
},
{
@@ -413,8 +413,8 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
Limit: 10,
},
expected: TestExpected{
Args: []any{uint64(1747945619), uint64(1747983448), "%1.65%", 1.65, "%1.65%", 1.65, "%1.65%", 1.65, "%1.65%", 1.65, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true name=education[].parameters,context=body,datatype=[]dynamic,materialized=true]."},
Args: []any{ "%1.65%", 1.65, "%1.65%", 1.65, "%1.65%", 1.65, "%1.65%", 1.65, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,materialized=true,jsondatatype=Array(Dynamic)]."},
},
expectedErr: nil,
},
@@ -440,8 +440,8 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
Limit: 10,
},
expected: TestExpected{
Args: []any{uint64(1747945619), uint64(1747983448), "%true%", true, "%true%", true, "%true%", true, "%true%", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true name=education[].parameters,context=body,datatype=[]dynamic,materialized=true]."},
Args: []any{ "%true%", true, "%true%", true, "%true%", true, "%true%", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,materialized=true,jsondatatype=Array(Dynamic)]."},
},
expectedErr: nil,
},
@@ -454,8 +454,8 @@ func TestStatementBuilderListQueryBodyPromoted(t *testing.T) {
Limit: 10,
},
expected: TestExpected{
Args: []any{uint64(1747945619), uint64(1747983448), "%passed%", "passed", "%passed%", "passed", "%passed%", "passed", "%passed%", "passed", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true name=education[].parameters,context=body,datatype=[]dynamic,materialized=true]."},
Args: []any{ "%passed%", "passed", "%passed%", "passed", "%passed%", "passed", "%passed%", "passed", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,materialized=true,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,materialized=true,jsondatatype=Array(Dynamic)]."},
},
expectedErr: nil,
},
@@ -549,7 +549,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "((NOT arrayExists(`body_v2.education`-> toFloat64OrNull(dynamicElement(`body_v2.education`.`type`, 'String')) = ?, dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND (NOT arrayExists(`body_v2.education`-> dynamicElement(`body_v2.education`.`type`, 'Int64') = ?, dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))))",
Args: []any{int64(10001), int64(10001), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].type` is ambiguous, found 2 different combinations of field context / data type: [name=education[].type,context=body,datatype=string name=education[].type,context=body,datatype=int64]."},
Warnings: []string{"Key `education[].type` is ambiguous, found 2 different combinations of field context / data type: [name=education[].type,context=body,datatype=string,jsondatatype=String name=education[].type,context=body,datatype=int64,jsondatatype=Int64]."},
},
},
{
@@ -576,7 +576,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))) OR arrayExists(x -> accurateCastOrNull(x, 'Float64') = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')))",
Args: []any{"%1.65%", 1.65, "%1.65%", 1.65, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
},
{
@@ -585,7 +585,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((arrayExists(`body_v2.education`-> arrayExists(x -> toString(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> arrayExists(x -> toString(x) = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')))",
Args: []any{"passed", "passed", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
},
{
@@ -594,7 +594,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toString(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))) OR arrayExists(x -> toString(x) = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')))",
Args: []any{"%passed%", "passed", "%passed%", "passed", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
},
{
@@ -603,7 +603,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((arrayExists(`body_v2.education`-> arrayExists(x -> toFloat64(x) IN (?, ?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')) OR ((arrayExists(`body_v2.education`-> arrayExists(x -> toString(x) IN (?, ?), arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'education')))",
Args: []any{1.65, 1.99, "1.65", "1.99", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
},
{
@@ -612,7 +612,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "((NOT arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND (NOT arrayExists(`body_v2.education`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'))) OR arrayExists(x -> accurateCastOrNull(x, 'Float64') = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)')))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))))",
Args: []any{"%1.65%", float64(1.65), "%1.65%", float64(1.65), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic]."},
Warnings: []string{"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)]."},
},
},
{
@@ -622,7 +622,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
WhereClause: "(has(arrayFlatten(arrayConcat(arrayMap(`body_v2.education`->dynamicElement(`body_v2.education`.`parameters`, 'Array(Nullable(Float64))'), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))), ?) OR has(arrayFlatten(arrayConcat(arrayMap(`body_v2.education`->dynamicElement(`body_v2.education`.`parameters`, 'Array(Dynamic)'), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))), ?))",
Args: []any{1.65, 1.65, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{
"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64 name=education[].parameters,context=body,datatype=[]dynamic].",
"Key `education[].parameters` is ambiguous, found 2 different combinations of field context / data type: [name=education[].parameters,context=body,datatype=[]float64,jsondatatype=Array(Nullable(Float64)) name=education[].parameters,context=body,datatype=[]dynamic,jsondatatype=Array(Dynamic)].",
},
},
},
@@ -702,7 +702,7 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
expected: TestExpected{
WhereClause: "(((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))')) OR arrayExists(x -> toFloat64(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(Int64))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'interests')) OR ((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(`body_v2.interests[].entities[].reviews`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata`-> arrayExists(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`-> (arrayExists(x -> LOWER(x) LIKE LOWER(?), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))')) OR arrayExists(x -> toFloat64OrNull(x) = ?, dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata[].positions`.`ratings`, 'Array(Nullable(String))'))), dynamicElement(`body_v2.interests[].entities[].reviews[].entries[].metadata`.`positions`, 'Array(JSON(max_dynamic_types=0, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews[].entries`.`metadata`, 'Array(JSON(max_dynamic_types=1, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities[].reviews`.`entries`, 'Array(JSON(max_dynamic_types=2, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests[].entities`.`reviews`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'interests')))",
Args: []any{"%4%", float64(4), "%4%", float64(4), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
Warnings: []string{"Key `interests[].entities[].reviews[].entries[].metadata[].positions[].ratings` is ambiguous, found 2 different combinations of field context / data type: [name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]int64 name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]string]."},
Warnings: []string{"Key `interests[].entities[].reviews[].entries[].metadata[].positions[].ratings` is ambiguous, found 2 different combinations of field context / data type: [name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]int64,jsondatatype=Array(Nullable(Int64)) name=interests[].entities[].reviews[].entries[].metadata[].positions[].ratings,context=body,datatype=[]string,jsondatatype=Array(Nullable(String))]."},
},
},
{
@@ -761,35 +761,27 @@ func TestJSONStmtBuilder_ArrayPaths(t *testing.T) {
},
},
{
name: "dynamic_array_element_compare_CONTAINS",
filter: "interests[].entities[].product_codes Contains 1",
name: "dynamic array element comparison",
filter: "ids Contains 1",
expected: TestExpected{
WhereClause: "((arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> (arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.interests[].entities`.`product_codes`, 'Array(Dynamic)'))) OR arrayExists(x -> accurateCastOrNull(x, 'Float64') = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.interests[].entities`.`product_codes`, 'Array(Dynamic)')))), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))'))) AND has(JSONAllPaths(body_v2), 'interests'))",
WhereClause: "(((arrayExists(x -> LOWER(toString(x)) LIKE LOWER(?), arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(body_v2.`ids`, 'Array(Dynamic)'))) OR arrayExists(x -> accurateCastOrNull(x, 'Float64') = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(body_v2.`ids`, 'Array(Dynamic)'))))) AND has(JSONAllPaths(body_v2), 'ids'))",
Args: []any{"%1%", float64(1), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
{
name: "dynamic_array_element_compare_HAS_STRING",
filter: "has(interests[].entities[].product_codes, '2002')",
name: "dynamic array element comparison",
filter: "ids != '1'",
expected: TestExpected{
WhereClause: "has(arrayFlatten(arrayConcat(arrayMap(`body_v2.interests`->arrayMap(`body_v2.interests[].entities`->dynamicElement(`body_v2.interests[].entities`.`product_codes`, 'Array(Dynamic)'), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))), ?)",
Args: []any{"2002", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
{
name: "dynamic_array_element_compare_NOT_EQUAL",
filter: "interests[].entities[].product_codes != '1'",
expected: TestExpected{
WhereClause: "(NOT arrayExists(`body_v2.interests`-> arrayExists(`body_v2.interests[].entities`-> arrayExists(x -> accurateCastOrNull(x, 'Float64') = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(`body_v2.interests[].entities`.`product_codes`, 'Array(Dynamic)'))), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))",
WhereClause: "(NOT arrayExists(x -> accurateCastOrNull(x, 'Float64') = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(body_v2.`ids`, 'Array(Dynamic)'))))",
Args: []any{int64(1), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
{
name: "dynamic_array_element_compare_HAS_INT",
filter: "has(interests[].entities[].product_codes, 1001)",
name: "dynamic array element comparison boolean",
filter: "ids = true",
expected: TestExpected{
WhereClause: "has(arrayFlatten(arrayConcat(arrayMap(`body_v2.interests`->arrayMap(`body_v2.interests[].entities`->dynamicElement(`body_v2.interests[].entities`.`product_codes`, 'Array(Dynamic)'), dynamicElement(`body_v2.interests`.`entities`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), dynamicElement(body_v2.`interests`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))), ?)",
Args: []any{float64(1001), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
WhereClause: "((arrayExists(x -> accurateCastOrNull(x, 'Bool') = ?, arrayFilter(x->(dynamicType(x) IN ('String', 'Int64', 'Float64', 'Bool')), dynamicElement(body_v2.`ids`, 'Array(Dynamic)')))) AND has(JSONAllPaths(body_v2), 'ids'))",
Args: []any{true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
}
@@ -836,7 +828,7 @@ func TestJSONStmtBuilder_IndexedPaths(t *testing.T) {
Limit: 10,
},
expected: TestExpected{
WhereClause: "((lower(assumeNotNull(dynamicElement(body_v2.user.name, 'String'))) = ?) AND has(JSONAllPaths(body_v2), 'user.name'))",
WhereClause: "((assumeNotNull(dynamicElement(body_v2.`user.name`, 'String')) = ?) AND has(JSONAllPaths(body_v2), 'user.name'))",
Args: []any{"alice", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
@@ -848,14 +840,11 @@ func TestJSONStmtBuilder_IndexedPaths(t *testing.T) {
Limit: 10,
},
expected: TestExpected{
WhereClause: "((toFloat64(assumeNotNull(dynamicElement(body_v2.user.address.zip, 'Int64'))) = ?) AND has(JSONAllPaths(body_v2), 'user.address.zip'))",
WhereClause: "((toFloat64(assumeNotNull(dynamicElement(body_v2.`user.address.zip`, 'Int64'))) = ?) AND has(JSONAllPaths(body_v2), 'user.address.zip'))",
Args: []any{float64(110001), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
// ── indexed exists: index is skipped; emits plain IS NOT NULL ──────────
// EXISTS/NOT EXISTS bypass the index because indexed columns store a default
// empty value for absent fields, making != "" unreliable for existence checks
// (a field with value "" would be incorrectly excluded).
// ── indexed exists: emits assumeNotNull != nil AND dynamicElement IS NOT NULL ─
{
name: "Indexed String Exists",
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
@@ -864,8 +853,8 @@ func TestJSONStmtBuilder_IndexedPaths(t *testing.T) {
Limit: 10,
},
expected: TestExpected{
WhereClause: "(dynamicElement(body_v2.`user.name`, 'String') IS NOT NULL)",
Args: []any{"1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
WhereClause: "((assumeNotNull(dynamicElement(body_v2.`user.name`, 'String')) <> ? AND dynamicElement(body_v2.`user.name`, 'String') IS NOT NULL))",
Args: []any{"", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
// ── indexed not-equal: assumeNotNull wrapping + no path index ─────────
@@ -877,10 +866,26 @@ func TestJSONStmtBuilder_IndexedPaths(t *testing.T) {
Limit: 10,
},
expected: TestExpected{
WhereClause: "(lower(assumeNotNull(dynamicElement(body_v2.user.name, 'String'))) <> ?)",
WhereClause: "(assumeNotNull(dynamicElement(body_v2.`user.name`, 'String')) <> ?)",
Args: []any{"alice", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
// ── indexed not-exists: assumeNotNull = "" AND assumeNotNull IS NOT NULL ─
// FilterOperatorNotExists → Equal + emptyValue("") in the indexed branch,
// then a second condition flipped to Exists (IS NOT NULL) on the same
// assumeNotNull expr, producing AND(= "", IS NOT NULL).
{
name: "Indexed String NotExists",
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "body.user.name NOT EXISTS"},
Limit: 10,
},
expected: TestExpected{
WhereClause: "((assumeNotNull(dynamicElement(body_v2.`user.name`, 'String')) = ? AND dynamicElement(body_v2.`user.name`, 'String') IS NULL))",
Args: []any{"", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
// ── special-character indexed paths ───────────────────────────────────
{
@@ -891,7 +896,7 @@ func TestJSONStmtBuilder_IndexedPaths(t *testing.T) {
Limit: 10,
},
expected: TestExpected{
WhereClause: "(((toFloat64(assumeNotNull(dynamicElement(body_v2.`http-status`, 'Int64'))) = ?) AND has(JSONAllPaths(body_v2), 'http-status')) OR ((toFloat64OrNull(lower(assumeNotNull(dynamicElement(body_v2.`http-status`, 'String')))) = ?) AND has(JSONAllPaths(body_v2), 'http-status')))",
WhereClause: "(((toFloat64(assumeNotNull(dynamicElement(body_v2.`http-status`, 'Int64'))) = ?) AND has(JSONAllPaths(body_v2), 'http-status')) OR ((toFloat64OrNull(assumeNotNull(dynamicElement(body_v2.`http-status`, 'String'))) = ?) AND has(JSONAllPaths(body_v2), 'http-status')))",
Args: []any{float64(200), float64(200), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
@@ -903,7 +908,7 @@ func TestJSONStmtBuilder_IndexedPaths(t *testing.T) {
Limit: 10,
},
expected: TestExpected{
WhereClause: "((toFloat64(assumeNotNull(dynamicElement(body_v2.response.`time-taken`, 'Float64'))) > ?) AND has(JSONAllPaths(body_v2), 'response.time-taken'))",
WhereClause: "((toFloat64(assumeNotNull(dynamicElement(body_v2.`response.time-taken`, 'Float64'))) > ?) AND has(JSONAllPaths(body_v2), 'response.time-taken'))",
Args: []any{float64(1.5), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
@@ -937,192 +942,20 @@ func TestJSONStmtBuilder_IndexedPaths(t *testing.T) {
})
}
}
func TestJSONStmtBuilder_SelectField(t *testing.T) {
enable, disable := jsonQueryTestUtil(t)
enable()
defer disable()
statementBuilder := buildJSONTestStatementBuilder(t, false)
cases := []struct {
name string
requestType qbtypes.RequestType
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
expected qbtypes.Statement
expectedErrContains string
}{
{
name: "select_x_education[].awards[].participated[].members",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Limit: 10,
SelectFields: []telemetrytypes.TelemetryFieldKey{
{
Name: "education[].awards[].participated[].members",
},
},
Filter: &qbtypes.Filter{
Expression: "user.name exists",
},
},
expected: qbtypes.Statement{
Query: "SELECT timestamp, id, arrayFlatten(arrayConcat(arrayMap(`body_v2.education`->arrayConcat(arrayMap(`body_v2.education[].awards`->arrayConcat(arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), arrayMap(`body_v2.education[].awards`->arrayConcat(arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')), arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AS `education[].awards[].participated[].members` FROM signoz_logs.distributed_logs_v2 WHERE (dynamicElement(body_v2.`user.name`, 'String') IS NOT NULL) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
{
name: "select_x_education[].awards[].type",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Limit: 10,
SelectFields: []telemetrytypes.TelemetryFieldKey{
{
Name: "education[].awards[].type",
},
},
Filter: &qbtypes.Filter{
Expression: "user.name exists",
},
},
expected: qbtypes.Statement{
Query: "SELECT timestamp, id, arrayFlatten(arrayConcat(arrayMap(`body_v2.education`->arrayConcat(arrayMap(`body_v2.education[].awards`->dynamicElement(`body_v2.education[].awards`.`type`, 'String'), dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), arrayMap(`body_v2.education[].awards`->dynamicElement(`body_v2.education[].awards`.`type`, 'String'), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AS `education[].awards[].type` FROM signoz_logs.distributed_logs_v2 WHERE (dynamicElement(body_v2.`user.name`, 'String') IS NOT NULL) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
{
name: "select_x_user.name",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Limit: 10,
SelectFields: []telemetrytypes.TelemetryFieldKey{
{
Name: "user.name",
},
},
},
expected: qbtypes.Statement{
Query: "SELECT timestamp, id, dynamicElement(body_v2.`user.name`, 'String') AS `user.name` FROM signoz_logs.distributed_logs_v2 WHERE timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
if c.expectedErrContains != "" {
require.Error(t, err)
require.Contains(t, err.Error(), c.expectedErrContains)
} else {
require.NoError(t, err)
require.Equal(t, c.expected.Query, q.Query)
require.Equal(t, c.expected.Args, q.Args)
require.Equal(t, c.expected.Warnings, q.Warnings)
}
})
}
}
func TestJSONStmtBuilder_OrderBy(t *testing.T) {
enable, disable := jsonQueryTestUtil(t)
enable()
defer disable()
statementBuilder := buildJSONTestStatementBuilder(t, false)
cases := []struct {
name string
requestType qbtypes.RequestType
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]
expected qbtypes.Statement
expectedErrContains string
}{
{
name: "order_by_education[].awards[].participated[].members",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Limit: 10,
Order: []qbtypes.OrderBy{
{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "education[].awards[].participated[].members",
},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
Filter: &qbtypes.Filter{
Expression: "user.name exists",
},
},
expected: qbtypes.Statement{
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE (dynamicElement(body_v2.`user.name`, 'String') IS NOT NULL) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY arrayFlatten(arrayConcat(arrayMap(`body_v2.education`->arrayConcat(arrayMap(`body_v2.education[].awards`->arrayConcat(arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=4, max_dynamic_paths=0))')), arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), dynamicElement(`body_v2.education`.`awards`, 'Array(JSON(max_dynamic_types=8, max_dynamic_paths=0))')), arrayMap(`body_v2.education[].awards`->arrayConcat(arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=256))')), arrayMap(`body_v2.education[].awards[].participated`->dynamicElement(`body_v2.education[].awards[].participated`.`members`, 'Array(Nullable(String))'), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education[].awards`.`participated`, 'Array(Dynamic)'))))), arrayMap(x->assumeNotNull(dynamicElement(x, 'JSON')), arrayFilter(x->(dynamicType(x) = 'JSON'), dynamicElement(`body_v2.education`.`awards`, 'Array(Dynamic)'))))), dynamicElement(body_v2.`education`, 'Array(JSON(max_dynamic_types=16, max_dynamic_paths=0))')))) AS `education[].awards[].participated[].members` asc LIMIT ?",
Args: []any{"1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
{
name: "order_by_user.name",
requestType: qbtypes.RequestTypeRaw,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Limit: 10,
Order: []qbtypes.OrderBy{
{
Key: qbtypes.OrderByKey{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "user.name",
},
},
Direction: qbtypes.OrderDirectionAsc,
},
},
},
expected: qbtypes.Statement{
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body_v2 as body, attributes_string, attributes_number, attributes_bool, resources_string, scope_string FROM signoz_logs.distributed_logs_v2 WHERE timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY dynamicElement(body_v2.`user.name`, 'String') AS `user.name` asc LIMIT ?",
Args: []any{"1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
q, err := statementBuilder.Build(context.Background(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
if c.expectedErrContains != "" {
require.Error(t, err)
require.Contains(t, err.Error(), c.expectedErrContains)
} else {
require.NoError(t, err)
require.Equal(t, c.expected.Query, q.Query)
require.Equal(t, c.expected.Args, q.Args)
require.Equal(t, c.expected.Warnings, q.Warnings)
}
})
}
}
func buildTestTelemetryMetadataStore(t *testing.T, addIndexes bool) *telemetrytypestest.MockMetadataStore {
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
mockMetadataStore.SetStaticFields(IntrinsicFields)
types, _ := telemetrytypes.TestJSONTypeSet()
for path, fieldDataTypes := range types {
for _, fdt := range fieldDataTypes {
for path, jsonTypes := range types {
for _, jsonType := range jsonTypes {
key := &telemetrytypes.TelemetryFieldKey{
Name: path,
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextBody,
FieldDataType: fdt,
FieldDataType: telemetrytypes.MappingJSONDataTypeToFieldDataType[jsonType],
JSONDataType: &jsonType,
}
if addIndexes {
jsonType := telemetrytypes.MappingFieldDataTypeToJSONDataType[fdt]
idx := slices.IndexFunc(telemetrytypes.TestIndexedPaths, func(entry telemetrytypes.TestIndexedPathEntry) bool {
return entry.Path == path && entry.Type == jsonType
})

View File

@@ -875,6 +875,7 @@ func TestAdjustKey(t *testing.T) {
require.Equal(t, c.expectedKey.FieldContext, key.FieldContext, "field context should match")
require.Equal(t, c.expectedKey.FieldDataType, key.FieldDataType, "field data type should match")
require.Equal(t, c.expectedKey.Materialized, key.Materialized, "materialized should match")
require.Equal(t, c.expectedKey.JSONDataType, key.JSONDataType, "json data type should match")
require.Equal(t, c.expectedKey.Indexes, key.Indexes, "json exists should match")
})
}

View File

@@ -21,84 +21,133 @@ import (
)
var (
defaultPathLimit = 100 // Default limit to prevent full table scans
CodeUnknownJSONDataType = errors.MustNewCode("unknown_json_data_type")
CodeFailLoadPromotedPaths = errors.MustNewCode("fail_load_promoted_paths")
CodeFailCheckPathPromoted = errors.MustNewCode("fail_check_path_promoted")
CodeFailIterateBodyJSONKeys = errors.MustNewCode("fail_iterate_body_json_keys")
CodeFailExtractBodyJSONKeys = errors.MustNewCode("fail_extract_body_json_keys")
CodeFailLoadLogsJSONIndexes = errors.MustNewCode("fail_load_logs_json_indexes")
CodeFailListJSONValues = errors.MustNewCode("fail_list_json_values")
CodeFailScanJSONValue = errors.MustNewCode("fail_scan_json_value")
CodeFailScanVariant = errors.MustNewCode("fail_scan_variant")
CodeFailBuildJSONPathsQuery = errors.MustNewCode("fail_build_json_paths_query")
CodeNoPathsToQueryIndexes = errors.MustNewCode("no_paths_to_query_indexes_provided")
CodeFailedToPrepareBatch = errors.MustNewCode("failed_to_prepare_batch_promoted_paths")
CodeFailedToSendBatch = errors.MustNewCode("failed_to_send_batch_promoted_paths")
CodeFailedToAppendPath = errors.MustNewCode("failed_to_append_path_promoted_paths")
)
// enrichJSONKeys enriches body-context keys with promoted path info, indexes,
// and JSON access plans. parentTypeCache contains parent array types (ArrayJSON/ArrayDynamic)
// pre-fetched in the main UNION query.
// fetchBodyJSONPaths extracts body JSON paths from the path_types table
// This function can be used by both JSONQueryBuilder and metadata extraction
// uniquePathLimit: 0 for no limit, >0 for maximum number of unique paths to return
// - For startup load: set to 10000 to get top 10k unique paths
// - For lookup: set to 0 (no limit needed for single path)
// - For metadata API: set to desired pagination limit
//
// NOTE: enrichment can not work with FuzzySelectors; QB requests exact matches for query building so
// parentTypeCache will actually have proper matches and
// FuzzyMatching is for Suggestions API so enrichment is not needed.
func (t *telemetryMetaStore) enrichJSONKeys(ctx context.Context, selectors []*telemetrytypes.FieldKeySelector, keys []*telemetrytypes.TelemetryFieldKey, parentTypeCache map[string][]telemetrytypes.FieldDataType) error {
mapOfExactSelectors := make(map[string]*telemetrytypes.FieldKeySelector)
for _, selector := range selectors {
if selector.SelectorMatchType != telemetrytypes.FieldSelectorMatchTypeExact {
continue
// searchOperator: LIKE for pattern matching, EQUAL for exact match.
func (t *telemetryMetaStore) fetchBodyJSONPaths(ctx context.Context,
fieldKeySelectors []*telemetrytypes.FieldKeySelector) ([]*telemetrytypes.TelemetryFieldKey, []string, bool, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.TelemetrySignal: telemetrytypes.SignalLogs.StringValue(),
instrumentationtypes.CodeNamespace: "metadata",
instrumentationtypes.CodeFunctionName: "fetchBodyJSONPaths",
})
query, args, limit := buildGetBodyJSONPathsQuery(fieldKeySelectors)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, nil, false, errors.WrapInternalf(err, CodeFailExtractBodyJSONKeys, "failed to extract body JSON keys")
}
defer rows.Close()
fieldKeys := []*telemetrytypes.TelemetryFieldKey{}
paths := []string{}
rowCount := 0
for rows.Next() {
var path string
var typesArray []string // ClickHouse returns array as []string
var lastSeen uint64
err = rows.Scan(&path, &typesArray, &lastSeen)
if err != nil {
return nil, nil, false, errors.WrapInternalf(err, CodeFailExtractBodyJSONKeys, "failed to scan body JSON key row")
}
mapOfExactSelectors[selector.Name] = selector
}
var filteredKeys []*telemetrytypes.TelemetryFieldKey
for _, key := range keys {
if key.FieldContext == telemetrytypes.FieldContextBody && mapOfExactSelectors[key.Name] != nil {
filteredKeys = append(filteredKeys, key)
for _, typ := range typesArray {
mapping, found := telemetrytypes.MappingStringToJSONDataType[typ]
if !found {
t.logger.ErrorContext(ctx, "failed to map type string to JSON data type", slog.String("type", typ), slog.String("path", path))
continue
}
fieldKeys = append(fieldKeys, &telemetrytypes.TelemetryFieldKey{
Name: path,
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextBody,
FieldDataType: telemetrytypes.MappingJSONDataTypeToFieldDataType[mapping],
JSONDataType: &mapping,
})
}
paths = append(paths, path)
rowCount++
}
if rows.Err() != nil {
return nil, nil, false, errors.WrapInternalf(rows.Err(), CodeFailIterateBodyJSONKeys, "error iterating body JSON keys")
}
if len(filteredKeys) == 0 {
return nil
}
paths := make([]string, 0, len(filteredKeys))
for _, key := range filteredKeys {
paths = append(paths, key.Name)
}
// fetch promoted paths
promoted, err := t.GetPromotedPaths(ctx, paths...)
if err != nil {
return err
}
// fetch JSON path indexes
indexes, err := t.getJSONPathIndexes(ctx, paths...)
if err != nil {
return err
}
// apply promoted/index metadata to keys
for _, key := range filteredKeys {
promotedKey := strings.Split(key.Name, telemetrytypes.ArraySep)[0]
key.Materialized = promoted[promotedKey]
key.Indexes = indexes[key.Name]
}
// build JSON access plans using the pre-fetched parent type cache
return t.buildJSONPlans(filteredKeys, parentTypeCache)
return fieldKeys, paths, rowCount <= limit, nil
}
// buildJSONPlans builds JSON access plans for the given keys
// using the provided parent type cache (pre-fetched in the main UNION query).
func (t *telemetryMetaStore) buildJSONPlans(keys []*telemetrytypes.TelemetryFieldKey, typeCache map[string][]telemetrytypes.FieldDataType) error {
if len(keys) == 0 {
return nil
func (t *telemetryMetaStore) buildBodyJSONPaths(ctx context.Context,
fieldKeySelectors []*telemetrytypes.FieldKeySelector) ([]*telemetrytypes.TelemetryFieldKey, bool, error) {
fieldKeys, paths, finished, err := t.fetchBodyJSONPaths(ctx, fieldKeySelectors)
if err != nil {
return nil, false, err
}
columnMeta := t.jsonColumnMetadata[telemetrytypes.SignalLogs][telemetrytypes.FieldContextBody]
promoted, err := t.GetPromotedPaths(ctx, paths...)
if err != nil {
return nil, false, err
}
indexes, err := t.getJSONPathIndexes(ctx, paths...)
if err != nil {
return nil, false, err
}
for _, fieldKey := range fieldKeys {
promotedKey := strings.Split(fieldKey.Name, telemetrytypes.ArraySep)[0]
fieldKey.Materialized = promoted[promotedKey]
fieldKey.Indexes = indexes[fieldKey.Name]
}
return fieldKeys, finished, t.buildJSONPlans(ctx, fieldKeys)
}
func (t *telemetryMetaStore) buildJSONPlans(ctx context.Context, keys []*telemetrytypes.TelemetryFieldKey) error {
parentSelectors := make([]*telemetrytypes.FieldKeySelector, 0, len(keys))
for _, key := range keys {
if err := key.SetJSONAccessPlan(columnMeta, typeCache); err != nil {
parentSelectors = append(parentSelectors, key.ArrayParentSelectors()...)
}
parentKeys, _, _, err := t.fetchBodyJSONPaths(ctx, parentSelectors)
if err != nil {
return err
}
typeCache := make(map[string][]telemetrytypes.JSONDataType)
for _, key := range parentKeys {
typeCache[key.Name] = append(typeCache[key.Name], *key.JSONDataType)
}
// build plans for keys now
for _, key := range keys {
err = key.SetJSONAccessPlan(t.jsonColumnMetadata[telemetrytypes.SignalLogs][telemetrytypes.FieldContextBody], typeCache)
if err != nil {
return err
}
}
@@ -106,6 +155,51 @@ func (t *telemetryMetaStore) buildJSONPlans(keys []*telemetrytypes.TelemetryFiel
return nil
}
func buildGetBodyJSONPathsQuery(fieldKeySelectors []*telemetrytypes.FieldKeySelector) (string, []any, int) {
if len(fieldKeySelectors) == 0 {
return "", nil, defaultPathLimit
}
from := fmt.Sprintf("%s.%s", DBName, PathTypesTableName)
// Build a better query using GROUP BY to deduplicate at database level
// This aggregates all types per path and gets the max last_seen, then applies LIMIT
sb := sqlbuilder.Select(
"path",
"groupArray(DISTINCT type) AS types",
"max(last_seen) AS last_seen",
).From(from)
limit := 0
// Add search filter if provided
orClauses := []string{}
for _, fieldKeySelector := range fieldKeySelectors {
// replace [*] with []
fieldKeySelector.Name = strings.ReplaceAll(fieldKeySelector.Name, telemetrytypes.ArrayAnyIndex, telemetrytypes.ArraySep)
// Extract search text for body JSON keys
keyName := CleanPathPrefixes(fieldKeySelector.Name)
if fieldKeySelector.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
orClauses = append(orClauses, sb.Equal("path", keyName))
} else {
// Pattern matching for metadata API (defaults to LIKE behavior for other operators)
orClauses = append(orClauses, sb.ILike("path", fmt.Sprintf("%%%s%%", querybuilder.FormatValueForContains(keyName))))
}
limit += fieldKeySelector.Limit
}
sb.Where(sb.Or(orClauses...))
// Group by path to get unique paths with aggregated types
sb.GroupBy("path")
// Order by max last_seen to get most recent paths first
sb.OrderBy("last_seen DESC")
if limit == 0 {
limit = defaultPathLimit
}
sb.Limit(limit)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
return query, args, limit
}
func (t *telemetryMetaStore) getJSONPathIndexes(ctx context.Context, paths ...string) (map[string][]telemetrytypes.JSONDataTypeIndex, error) {
filteredPaths := []string{}
for _, path := range paths {

View File

@@ -7,9 +7,99 @@ import (
"github.com/SigNoz/signoz-otel-collector/constants"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetrylogs"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/require"
)
func TestBuildGetBodyJSONPathsQuery(t *testing.T) {
testCases := []struct {
name string
fieldKeySelectors []*telemetrytypes.FieldKeySelector
expectedSQL string
expectedArgs []any
expectedLimit int
}{
{
name: "Single search text with EQUAL operator",
fieldKeySelectors: []*telemetrytypes.FieldKeySelector{
{
Name: "user.name",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeExact,
},
},
expectedSQL: "SELECT path, groupArray(DISTINCT type) AS types, max(last_seen) AS last_seen FROM signoz_metadata.distributed_field_keys WHERE (path = ?) GROUP BY path ORDER BY last_seen DESC LIMIT ?",
expectedArgs: []any{"user.name", defaultPathLimit},
expectedLimit: defaultPathLimit,
},
{
name: "Single search text with LIKE operator",
fieldKeySelectors: []*telemetrytypes.FieldKeySelector{
{
Name: "user",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
},
},
expectedSQL: "SELECT path, groupArray(DISTINCT type) AS types, max(last_seen) AS last_seen FROM signoz_metadata.distributed_field_keys WHERE (LOWER(path) LIKE LOWER(?)) GROUP BY path ORDER BY last_seen DESC LIMIT ?",
expectedArgs: []any{"%user%", 100},
expectedLimit: 100,
},
{
name: "Multiple search texts with EQUAL operator",
fieldKeySelectors: []*telemetrytypes.FieldKeySelector{
{
Name: "user.name",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeExact,
},
{
Name: "user.age",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeExact,
},
},
expectedSQL: "SELECT path, groupArray(DISTINCT type) AS types, max(last_seen) AS last_seen FROM signoz_metadata.distributed_field_keys WHERE (path = ? OR path = ?) GROUP BY path ORDER BY last_seen DESC LIMIT ?",
expectedArgs: []any{"user.name", "user.age", defaultPathLimit},
expectedLimit: defaultPathLimit,
},
{
name: "Multiple search texts with LIKE operator",
fieldKeySelectors: []*telemetrytypes.FieldKeySelector{
{
Name: "user",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
},
{
Name: "admin",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
},
},
expectedSQL: "SELECT path, groupArray(DISTINCT type) AS types, max(last_seen) AS last_seen FROM signoz_metadata.distributed_field_keys WHERE (LOWER(path) LIKE LOWER(?) OR LOWER(path) LIKE LOWER(?)) GROUP BY path ORDER BY last_seen DESC LIMIT ?",
expectedArgs: []any{"%user%", "%admin%", defaultPathLimit},
expectedLimit: defaultPathLimit,
},
{
name: "Search with Contains operator (should default to LIKE)",
fieldKeySelectors: []*telemetrytypes.FieldKeySelector{
{
Name: "test",
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
},
},
expectedSQL: "SELECT path, groupArray(DISTINCT type) AS types, max(last_seen) AS last_seen FROM signoz_metadata.distributed_field_keys WHERE (LOWER(path) LIKE LOWER(?)) GROUP BY path ORDER BY last_seen DESC LIMIT ?",
expectedArgs: []any{"%test%", defaultPathLimit},
expectedLimit: defaultPathLimit,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
query, args, limit := buildGetBodyJSONPathsQuery(tc.fieldKeySelectors)
require.Equal(t, tc.expectedSQL, query)
require.Equal(t, tc.expectedArgs, args)
require.Equal(t, tc.expectedLimit, limit)
})
}
}
func TestBuildListLogsJSONIndexesQuery(t *testing.T) {
testCases := []struct {
name string

View File

@@ -397,156 +397,90 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors
// tables to query based on field selectors
queryAttributeTable := false
queryResourceTable := false
queryBodyTable := false
for _, selector := range fieldKeySelectors {
if selector.FieldContext == telemetrytypes.FieldContextUnspecified {
// unspecified context, query all tables
// unspecified context, query both tables
queryAttributeTable = true
queryResourceTable = true
queryBodyTable = true
break
} else if selector.FieldContext == telemetrytypes.FieldContextAttribute {
queryAttributeTable = true
} else if selector.FieldContext == telemetrytypes.FieldContextResource {
queryResourceTable = true
} else if selector.FieldContext == telemetrytypes.FieldContextBody {
queryBodyTable = true
}
}
// body keys are gated behind the feature flag
queryBodyTable = queryBodyTable && querybuilder.BodyJSONQueryEnabled
// requestedFieldKeySelectors is the set of names the user explicitly asked for.
// Used to ensure a name that is both a parent path AND a directly requested field still surfaces
// in the result keys (e.g. "a.b[].c" is a parent of "a.b[].c[].d" but also a queried field).
mapOfRequestedSelectors := make(map[string]bool)
for _, sel := range fieldKeySelectors {
if sel.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
mapOfRequestedSelectors[sel.Name] = true
}
tablesToQuery := []struct {
fieldContext telemetrytypes.FieldContext
shouldQuery bool
}{
{telemetrytypes.FieldContextAttribute, queryAttributeTable},
{telemetrytypes.FieldContextResource, queryResourceTable},
}
// pre-compute parent array path names from body selectors for JSON plan building;
// these will be fetched as a separate UNION arm filtered to ArrayJSON/ArrayDynamic only.
parentPaths := make(map[string]bool)
if queryBodyTable {
for _, sel := range fieldKeySelectors {
if sel.FieldContext != telemetrytypes.FieldContextBody &&
sel.FieldContext != telemetrytypes.FieldContextUnspecified {
continue
}
key := &telemetrytypes.TelemetryFieldKey{
Name: sel.Name,
Signal: telemetrytypes.SignalLogs,
FieldContext: telemetrytypes.FieldContextBody,
}
if !key.KeyNameContainsArray() {
continue
}
for _, parent := range key.ArrayParentPaths() {
parentPaths[parent] = true
}
for _, table := range tablesToQuery {
if !table.shouldQuery {
continue
}
fieldContext := table.fieldContext
// table name based on field context
var tblName string
if fieldContext == telemetrytypes.FieldContextAttribute {
tblName = t.logsDBName + "." + t.logAttributeKeysTblName
} else {
tblName = t.logsDBName + "." + t.logResourceKeysTblName
}
}
// attribute and resource tables share identical schema (name/datatype columns, lower-cased select,
// TagDataType encoding) — only the table name and field context differ.
addTagTableQuery := func(tblName string, fieldContext telemetrytypes.FieldContext) {
sb := sqlbuilder.Select(
"name AS tag_key",
fmt.Sprintf("'%s' AS tag_type", fieldContext.TagType()),
"lower(datatype) AS tag_data_type", // logs had historical mixed-case data
fmt.Sprintf("%d AS priority", getPriorityForContext(fieldContext)),
"lower(datatype) AS tag_data_type", // in logs, we had some historical data with capital and small case
fmt.Sprintf(`%d AS priority`, getPriorityForContext(fieldContext)),
).From(tblName)
var limit int
conds := []string{}
for _, sel := range fieldKeySelectors {
for _, fieldKeySelector := range fieldKeySelectors {
// Include this selector if:
// 1. It has unspecified context (matches all tables)
// 2. Its context matches the current table's context
if sel.FieldContext != telemetrytypes.FieldContextUnspecified && sel.FieldContext != fieldContext {
if fieldKeySelector.FieldContext != telemetrytypes.FieldContextUnspecified &&
fieldKeySelector.FieldContext != fieldContext {
continue
}
// key part of the selector
fieldKeyConds := []string{}
if sel.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
fieldKeyConds = append(fieldKeyConds, sb.E("name", sel.Name))
if fieldKeySelector.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
fieldKeyConds = append(fieldKeyConds, sb.E("name", fieldKeySelector.Name))
} else {
fieldKeyConds = append(fieldKeyConds, sb.ILike("name", "%"+escapeForLike(sel.Name)+"%"))
fieldKeyConds = append(fieldKeyConds, sb.ILike("name", "%"+escapeForLike(fieldKeySelector.Name)+"%"))
}
if sel.FieldDataType != telemetrytypes.FieldDataTypeUnspecified {
fieldKeyConds = append(fieldKeyConds, sb.E("datatype", sel.FieldDataType.TagDataType()))
// now look at the field data type
if fieldKeySelector.FieldDataType != telemetrytypes.FieldDataTypeUnspecified {
fieldKeyConds = append(fieldKeyConds, sb.E("datatype", fieldKeySelector.FieldDataType.TagDataType()))
}
if len(fieldKeyConds) > 0 {
conds = append(conds, sb.And(fieldKeyConds...))
}
limit += fieldKeySelector.Limit
}
if len(conds) > 0 {
sb.Where(sb.Or(conds...))
}
sb.GroupBy("name", "datatype")
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
queries = append(queries, query)
allArgs = append(allArgs, args...)
}
if queryAttributeTable {
addTagTableQuery(t.logsDBName+"."+t.logAttributeKeysTblName, telemetrytypes.FieldContextAttribute)
}
if queryResourceTable {
addTagTableQuery(t.logsDBName+"."+t.logResourceKeysTblName, telemetrytypes.FieldContextResource)
}
// body context uses a different table/schema (field_name, field_data_type) and requires
// signal+context base filters. It also fetches parent array container types (ArrayJSON/ArrayDynamic)
// needed for JSON access plan building.
if queryBodyTable {
sb := sqlbuilder.Select(
"field_name AS tag_key",
fmt.Sprintf("'%s' AS tag_type", telemetrytypes.FieldContextBody.TagType()),
"field_data_type AS tag_data_type",
fmt.Sprintf("%d AS priority", getPriorityForContext(telemetrytypes.FieldContextBody)),
).From(fmt.Sprintf("%s.%s", DBName, FieldKeysTable))
sb.Where(sb.E("signal", telemetrytypes.SignalLogs.StringValue()))
sb.Where(sb.E("field_context", telemetrytypes.FieldContextBody.StringValue()))
branches := []string{}
for _, sel := range fieldKeySelectors {
if sel.FieldContext != telemetrytypes.FieldContextUnspecified && sel.FieldContext != telemetrytypes.FieldContextBody {
continue
}
fieldKeyConds := []string{}
if sel.SelectorMatchType == telemetrytypes.FieldSelectorMatchTypeExact {
fieldKeyConds = append(fieldKeyConds, sb.E("field_name", sel.Name))
} else {
fieldKeyConds = append(fieldKeyConds, sb.ILike("field_name", "%"+escapeForLike(sel.Name)+"%"))
}
if sel.FieldDataType != telemetrytypes.FieldDataTypeUnspecified {
fieldKeyConds = append(fieldKeyConds, sb.E("field_data_type", sel.FieldDataType.StringValue()))
}
if len(fieldKeyConds) > 0 {
branches = append(branches, sb.And(fieldKeyConds...))
}
if limit == 0 {
limit = 1000
}
if len(parentPaths) > 0 {
names := make([]any, 0, len(parentPaths))
for n := range parentPaths {
names = append(names, n)
}
branches = append(branches, sb.And(
sb.In("field_name", names...),
sb.In("field_data_type",
telemetrytypes.FieldDataTypeArrayDynamic.StringValue(),
telemetrytypes.FieldDataTypeArrayJSON.StringValue(),
),
))
}
if len(branches) > 0 {
sb.Where(sb.Or(branches...))
}
sb.GroupBy("field_name", "field_data_type")
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
queries = append(queries, query)
allArgs = append(allArgs, args...)
@@ -583,7 +517,6 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors
defer rows.Close()
keys := []*telemetrytypes.TelemetryFieldKey{}
parentTypes := make(map[string][]telemetrytypes.FieldDataType)
rowCount := 0
searchTexts := []string{}
@@ -607,21 +540,6 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors
if err != nil {
return nil, false, errors.Wrap(err, errors.TypeInternal, errors.CodeInternal, ErrFailedToGetLogsKeys.Error())
}
// ArrayJSON/ArrayDynamic body rows for parent paths are needed by the JSON access plan
// builder (enrichJSONKeys). Always record them in parentTypes. Only skip adding to keys
// if the user did not also directly request this name — a field like "education" can be
// both a parent of "education[].name" and an explicitly queried field in its own right.
switch fieldDataType {
case telemetrytypes.FieldDataTypeArrayJSON, telemetrytypes.FieldDataTypeArrayDynamic:
if fieldContext == telemetrytypes.FieldContextBody && parentPaths[name] {
parentTypes[name] = append(parentTypes[name], fieldDataType)
if !mapOfRequestedSelectors[name] {
continue // skip; don't register the key.
}
}
}
key, ok := mapOfKeys[name+";"+fieldContext.StringValue()+";"+fieldDataType.StringValue()]
// if there is no materialised column, create a key with the field context and data type
@@ -675,11 +593,13 @@ func (t *telemetryMetaStore) getLogsKeys(ctx context.Context, fieldKeySelectors
}
}
// enrich body keys with promoted paths, indexes, and JSON access plans
if querybuilder.BodyJSONQueryEnabled {
if err := t.enrichJSONKeys(ctx, fieldKeySelectors, keys, parentTypes); err != nil {
return nil, false, err
bodyJSONPaths, finished, err := t.buildBodyJSONPaths(ctx, fieldKeySelectors) // LIKE for pattern matching
if err != nil {
t.logger.ErrorContext(ctx, "failed to extract body JSON paths", errors.Attr(err))
}
keys = append(keys, bodyJSONPaths...)
complete = complete && finished
}
if _, err := t.updateColumnEvolutionMetadataForKeys(ctx, keys); err != nil {

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