Compare commits

..

1 Commits

Author SHA1 Message Date
ahrefabhi
63b9331c78 feat: added initial session recording changes 2025-09-01 12:06:21 +05:30
75 changed files with 5972 additions and 2461 deletions

View File

@@ -26,7 +26,7 @@ const config: Config.InitialOptions = {
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
],
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],

View File

@@ -48,9 +48,6 @@
"@signozhq/design-tokens": "1.1.4",
"@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0",
"@signozhq/sonner": "0.1.0",
"@signozhq/table": "0.3.7",
"@signozhq/tooltip": "0.0.2",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@uiw/codemirror-theme-copilot": "4.23.11",
@@ -136,6 +133,7 @@
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",
"rrweb-player": "1.0.0-alpha.4",
"stream": "^0.0.2",
"style-loader": "1.3.0",
"styled-components": "^5.3.11",
@@ -176,6 +174,7 @@
"@commitlint/config-conventional": "^16.2.4",
"@faker-js/faker": "9.3.0",
"@jest/globals": "^27.5.1",
"@rrweb/types": "2.0.0-alpha.18",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "14.4.3",

View File

@@ -295,3 +295,15 @@ export const MetricsExplorer = Loadable(
export const ApiMonitoring = Loadable(
() => import(/* webpackChunkName: "ApiMonitoring" */ 'pages/ApiMonitoring'),
);
export const SessionRecordings = Loadable(
() =>
import(/* webpackChunkName: "SessionRecordings" */ 'pages/SessionRecording'),
);
export const SessionRecordingsDetail = Loadable(
() =>
import(
/* webpackChunkName: "SessionRecordingsDetail" */ 'pages/SessionRecording/SessionDetail'
),
);

View File

@@ -54,6 +54,8 @@ import {
WorkspaceAccessRestricted,
WorkspaceBlocked,
WorkspaceSuspended,
SessionRecordings,
SessionRecordingsDetail,
} from './pageComponents';
const routes: AppRoutes[] = [
@@ -450,6 +452,20 @@ const routes: AppRoutes[] = [
key: 'METER_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.SESSION_RECORDINGS,
exact: true,
component: SessionRecordings,
key: 'SESSION_RECORDINGS',
isPrivate: true,
},
{
path: ROUTES.SESSION_RECORDINGS_DETAIL,
exact: true,
component: SessionRecordingsDetail,
key: 'SESSION_RECORDINGS_DETAIL',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER_VIEWS,
exact: true,

View File

@@ -1,25 +0,0 @@
import { ApiV2Instance } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps } from 'types/api/settings/getRetention';
// Only works for logs
const getRetentionV2 = async (): Promise<
SuccessResponseV2<PayloadProps<'logs'>>
> => {
try {
const response = await ApiV2Instance.get<PayloadProps<'logs'>>(
`/settings/ttl`,
);
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getRetentionV2;

View File

@@ -1,14 +1,14 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadPropsV2, Props } from 'types/api/settings/setRetention';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/settings/setRetention';
const setRetention = async (
props: Props,
): Promise<SuccessResponseV2<PayloadPropsV2>> => {
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post<PayloadPropsV2>(
const response = await axios.post<PayloadProps>(
`/settings/ttl?duration=${props.totalDuration}&type=${props.type}${
props.coldStorage
? `&coldStorage=${props.coldStorage}&toColdDuration=${props.toColdDuration}`
@@ -17,11 +17,13 @@ const setRetention = async (
);
return {
httpStatusCode: response.status,
data: response.data,
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,32 +0,0 @@
import { ApiV2Instance } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadPropsV2, PropsV2 } from 'types/api/settings/setRetention';
const setRetentionV2 = async ({
type,
defaultTTLDays,
coldStorageVolume,
coldStorageDuration,
ttlConditions,
}: PropsV2): Promise<SuccessResponseV2<PayloadPropsV2>> => {
try {
const response = await ApiV2Instance.post<PayloadPropsV2>(`/settings/ttl`, {
type,
defaultTTLDays,
coldStorageVolume,
coldStorageDuration,
ttlConditions,
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default setRetentionV2;

View File

@@ -23,7 +23,6 @@ import {
} from 'container/LogDetailedView/utils';
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
@@ -40,7 +39,7 @@ import {
TextSelect,
X,
} from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useCopyToClipboard, useLocation } from 'react-use';
import { AppState } from 'store/reducers';
@@ -95,8 +94,6 @@ function LogDetailInner({
const { notifications } = useNotifications();
const { onLogCopy } = useCopyLogLink(log?.id);
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
const handleModeChange = (e: RadioChangeEvent): void => {
@@ -149,34 +146,6 @@ function LogDetailInner({
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
};
const handleQueryExpressionChange = useCallback(
(value: string, queryIndex: number) => {
// update the query at the given index
setContextQuery((prev) => {
if (!prev) return prev;
return {
...prev,
builder: {
...prev.builder,
queryData: prev.builder.queryData.map((query, idx) =>
idx === queryIndex
? {
...query,
filter: {
...query.filter,
expression: value,
},
}
: query,
),
},
};
});
},
[],
);
const handleRunQuery = (expression: string): void => {
let updatedContextQuery = cloneDeep(contextQuery);
@@ -336,19 +305,11 @@ function LogDetailInner({
onClick={handleFilterVisible}
/>
)}
<Tooltip title="Copy Log Link" placement="left" aria-label="Copy Log Link">
<Button
className="action-btn"
icon={<Copy size={16} />}
onClick={onLogCopy}
/>
</Tooltip>
</div>
{isFilterVisible && contextQuery?.builder.queryData[0] && (
<div className="log-detail-drawer-query-container">
<QuerySearch
onChange={(value): void => handleQueryExpressionChange(value, 0)}
onChange={(): void => {}}
dataSource={DataSource.LOGS}
queryData={contextQuery?.builder.queryData[0]}
onRun={handleRunQuery}

View File

@@ -56,8 +56,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
.map(({ name }) => ({
title: name,
dataIndex: name,
accessorKey: name,
id: name.toLowerCase().replace(/\./g, '_'),
key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
@@ -85,10 +83,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
// We do not need any title and data index for the log state indicator
title: '',
dataIndex: '',
// eslint-disable-next-line sonarjs/no-duplicate-string
key: 'state-indicator',
accessorKey: 'state-indicator',
id: 'state-indicator',
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
children: (
<div className={cx('state-indicator', fontSize)}>
@@ -106,8 +101,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
title: 'timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
accessorKey: 'timestamp',
id: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886
render: (
field: string | number,
@@ -142,8 +135,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
title: 'body',
dataIndex: 'body',
key: 'body',
accessorKey: 'body',
id: 'body',
render: (
field: string | number,
): ColumnTypeRender<Record<string, unknown>> => ({

View File

@@ -33,5 +33,4 @@ export enum LOCALSTORAGE {
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
FUNNEL_STEPS = 'FUNNEL_STEPS',
LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES',
SHOW_FREQUENCY_CHART = 'SHOW_FREQUENCY_CHART',
}

View File

@@ -19,6 +19,8 @@ const ROUTES = {
GET_STARTED_AZURE_MONITORING: '/get-started/azure-monitoring',
USAGE_EXPLORER: '/usage-explorer',
APPLICATION: '/services',
SESSION_RECORDINGS: '/session-recordings',
SESSION_RECORDINGS_DETAIL: '/session-recordings/:sessionId',
ALL_DASHBOARD: '/dashboard',
DASHBOARD: '/dashboard/:dashboardId',
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',

View File

@@ -4,7 +4,6 @@
import './AppLayout.styles.scss';
import * as Sentry from '@sentry/react';
import { Toaster } from '@signozhq/sonner';
import { Flex } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
@@ -853,8 +852,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
{showChangelogModal && changelog && (
<ChangelogModal changelog={changelog} onClose={toggleChangelogModal} />
)}
<Toaster />
</Layout>
);
}

View File

@@ -1,6 +1,6 @@
.explorer-options-container {
position: fixed;
bottom: 0px;
bottom: 8px;
left: calc(50% + 240px);
transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear;
@@ -74,7 +74,6 @@
display: flex;
gap: 16px;
z-index: 1;
.ant-select-selector {
padding: 0 !important;
}

View File

@@ -27,7 +27,7 @@
}
.explorer-show-btn {
border-radius: 6px 6px 0px 0px;
border-radius: 10px 10px 0px 0px;
border: 1px solid var(--bg-slate-400);
background: rgba(22, 24, 29, 0.4);
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);

View File

@@ -2,30 +2,22 @@
import { LoadingOutlined } from '@ant-design/icons';
import { Button, Card, Col, Divider, Modal, Row, Spin, Typography } from 'antd';
import setRetentionApi from 'api/settings/setRetention';
import setRetentionApiV2 from 'api/settings/setRetentionV2';
import TextToolTip from 'components/TextToolTip';
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
import useComponentPermission from 'hooks/useComponentPermission';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
import { StatusCodes } from 'http-status-codes';
import find from 'lodash-es/find';
import { useAppContext } from 'providers/App/App';
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { useInterval } from 'react-use';
import {
ErrorResponse,
ErrorResponseV2,
SuccessResponse,
SuccessResponseV2,
} from 'types/api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
IDiskType,
PayloadProps as GetDisksPayload,
} from 'types/api/disks/getDisks';
import APIError from 'types/api/error';
import { TTTLType } from 'types/api/settings/common';
import {
PayloadPropsLogs as GetRetentionPeriodLogsPayload,
@@ -135,7 +127,7 @@ function GeneralSettings({
useEffect(() => {
if (logsCurrentTTLValues) {
setLogsTotalRetentionPeriod(logsCurrentTTLValues.default_ttl_days * 24);
setLogsTotalRetentionPeriod(logsCurrentTTLValues.logs_ttl_duration_hrs);
setLogsS3RetentionPeriod(
logsCurrentTTLValues.logs_move_ttl_duration_hrs
? logsCurrentTTLValues.logs_move_ttl_duration_hrs
@@ -344,40 +336,20 @@ function GeneralSettings({
}
try {
onPostApiLoadingHandler(type);
const setTTLResponse = await setRetentionApi({
type,
totalDuration: `${apiCallTotalRetention || -1}h`,
coldStorage: s3Enabled ? 's3' : null,
toColdDuration: `${apiCallS3Retention || -1}h`,
});
let hasSetTTLFailed = false;
try {
if (type === 'logs') {
await setRetentionApiV2({
type,
defaultTTLDays: apiCallTotalRetention ? apiCallTotalRetention / 24 : -1, // convert Hours to days
coldStorageVolume: '',
coldStorageDuration: 0,
ttlConditions: [],
});
} else {
await setRetentionApi({
type,
totalDuration: `${apiCallTotalRetention || -1}h`,
coldStorage: s3Enabled ? 's3' : null,
toColdDuration: `${apiCallS3Retention || -1}h`,
});
}
} catch (error) {
if (setTTLResponse.statusCode === 409) {
hasSetTTLFailed = true;
if ((error as APIError).getHttpStatusCode() === StatusCodes.CONFLICT) {
notifications.error({
message: 'Error',
description: t('retention_request_race_condition'),
placement: 'topRight',
});
} else {
notifications.error({
message: 'Error',
description: (error as APIError).getErrorMessage(),
placement: 'topRight',
});
}
notifications.error({
message: 'Error',
description: t('retention_request_race_condition'),
placement: 'topRight',
});
}
if (type === 'metrics') {
@@ -404,14 +376,11 @@ function GeneralSettings({
logsTtlValuesRefetch();
if (!hasSetTTLFailed)
// Updates the currentTTL Values in order to avoid pushing the same values.
setLogsCurrentTTLValues((prev) => ({
...prev,
setLogsCurrentTTLValues({
logs_ttl_duration_hrs: logsTotalRetentionPeriod || -1,
logs_move_ttl_duration_hrs: logsS3RetentionPeriod || -1,
default_ttl_days: logsTotalRetentionPeriod
? logsTotalRetentionPeriod / 24 // convert Hours to days
: -1,
}));
status: '',
});
}
} catch (error) {
notifications.error({
@@ -430,7 +399,6 @@ function GeneralSettings({
const renderConfig = [
{
name: 'Metrics',
type: 'metrics',
retentionFields: [
{
name: t('total_retention_period'),
@@ -472,7 +440,6 @@ function GeneralSettings({
},
{
name: 'Traces',
type: 'traces',
retentionFields: [
{
name: t('total_retention_period'),
@@ -512,7 +479,6 @@ function GeneralSettings({
},
{
name: 'Logs',
type: 'logs',
retentionFields: [
{
name: t('total_retention_period'),
@@ -571,7 +537,6 @@ function GeneralSettings({
/>
{category.retentionFields.map((retentionField) => (
<Retention
type={category.type as TTTLType}
key={retentionField.name}
text={retentionField.name}
retentionValue={retentionField.value}
@@ -660,7 +625,7 @@ interface GeneralSettingsProps {
ErrorResponse | SuccessResponse<GetRetentionPeriodTracesPayload>
>['refetch'];
logsTtlValuesRefetch: UseQueryResult<
ErrorResponseV2 | SuccessResponseV2<GetRetentionPeriodLogsPayload>
ErrorResponse | SuccessResponse<GetRetentionPeriodLogsPayload>
>['refetch'];
}

View File

@@ -9,7 +9,6 @@ import {
useRef,
useState,
} from 'react';
import { TTTLType } from 'types/api/settings/common';
import {
Input,
@@ -21,13 +20,11 @@ import {
convertHoursValueToRelevantUnit,
SettingPeriod,
TimeUnits,
TimeUnitsValues,
} from './utils';
const { Option } = Select;
function Retention({
type,
retentionValue,
setRetentionValue,
text,
@@ -53,9 +50,7 @@ function Retention({
if (!interacted.current) setSelectTimeUnit(initialTimeUnitValue);
}, [initialTimeUnitValue]);
const menuItems = TimeUnits.filter((option) =>
type === 'logs' ? option.value !== TimeUnitsValues.hr : true,
).map((option) => (
const menuItems = TimeUnits.map((option) => (
<Option key={option.value} value={option.value}>
{option.key}
</Option>
@@ -129,7 +124,6 @@ function Retention({
}
interface RetentionProps {
type: TTTLType;
retentionValue: number | null;
text: string;
setRetentionValue: Dispatch<SetStateAction<number | null>>;

View File

@@ -1,13 +1,11 @@
import { Typography } from 'antd';
import getDisks from 'api/disks/getDisks';
import getRetentionPeriodApi from 'api/settings/getRetention';
import getRetentionPeriodApiV2 from 'api/settings/getRetentionV2';
import Spinner from 'components/Spinner';
import { useAppContext } from 'providers/App/App';
import { useTranslation } from 'react-i18next';
import { useQueries } from 'react-query';
import { ErrorResponse, SuccessResponse, SuccessResponseV2 } from 'types/api';
import APIError from 'types/api/error';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { TTTLType } from 'types/api/settings/common';
import { PayloadProps as GetRetentionPeriodAPIPayloadProps } from 'types/api/settings/getRetention';
@@ -17,10 +15,6 @@ type TRetentionAPIReturn<T extends TTTLType> = Promise<
SuccessResponse<GetRetentionPeriodAPIPayloadProps<T>> | ErrorResponse
>;
type TRetentionAPIReturnV2<T extends TTTLType> = Promise<
SuccessResponseV2<GetRetentionPeriodAPIPayloadProps<T>>
>;
function GeneralSettings(): JSX.Element {
const { t } = useTranslation('common');
const { user } = useAppContext();
@@ -42,7 +36,7 @@ function GeneralSettings(): JSX.Element {
queryKey: ['getRetentionPeriodApiTraces', user?.accessJwt],
},
{
queryFn: (): TRetentionAPIReturnV2<'logs'> => getRetentionPeriodApiV2(), // Only works for logs
queryFn: (): TRetentionAPIReturn<'logs'> => getRetentionPeriodApi('logs'),
queryKey: ['getRetentionPeriodApiLogs', user?.accessJwt],
},
{
@@ -76,7 +70,7 @@ function GeneralSettings(): JSX.Element {
if (getRetentionPeriodLogsApiResponse.isError || getDisksResponse.isError) {
return (
<Typography>
{(getRetentionPeriodLogsApiResponse.error as APIError).getErrorMessage() ||
{getRetentionPeriodLogsApiResponse.data?.error ||
getDisksResponse.data?.error ||
t('something_went_wrong')}
</Typography>
@@ -92,7 +86,7 @@ function GeneralSettings(): JSX.Element {
getRetentionPeriodTracesApiResponse.isLoading ||
!getRetentionPeriodTracesApiResponse.data?.payload ||
getRetentionPeriodLogsApiResponse.isLoading ||
!getRetentionPeriodLogsApiResponse.data?.data
!getRetentionPeriodLogsApiResponse.data?.payload
) {
return <Spinner tip="Loading.." height="70vh" />;
}
@@ -105,7 +99,7 @@ function GeneralSettings(): JSX.Element {
metricsTtlValuesRefetch: getRetentionPeriodMetricsApiResponse.refetch,
tracesTtlValuesPayload: getRetentionPeriodTracesApiResponse.data?.payload,
tracesTtlValuesRefetch: getRetentionPeriodTracesApiResponse.refetch,
logsTtlValuesPayload: getRetentionPeriodLogsApiResponse.data?.data,
logsTtlValuesPayload: getRetentionPeriodLogsApiResponse.data?.payload,
logsTtlValuesRefetch: getRetentionPeriodLogsApiResponse.refetch,
}}
/>

View File

@@ -5,26 +5,19 @@ export interface ITimeUnit {
key: string;
multiplier: number;
}
export enum TimeUnitsValues {
hr = 'hr',
day = 'day',
month = 'month',
}
export const TimeUnits: ITimeUnit[] = [
{
value: TimeUnitsValues.hr,
value: 'hr',
key: 'Hours',
multiplier: 1,
},
{
value: TimeUnitsValues.day,
value: 'day',
key: 'Days',
multiplier: 1 / 24,
},
{
value: TimeUnitsValues.month,
value: 'month',
key: 'Months',
multiplier: 1 / (24 * 30),
},

View File

@@ -24,7 +24,7 @@ import { AnimatePresence } from 'motion/react';
import * as motion from 'motion/react-client';
import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useMutation, useQuery } from 'react-query';
import { UserPreference } from 'types/api/preferences/preference';
import { DataSource } from 'types/common/queryBuilder';
@@ -40,6 +40,9 @@ import HomeChecklist, { ChecklistItem } from './HomeChecklist/HomeChecklist';
import SavedViews from './SavedViews/SavedViews';
import Services from './Services/Services';
import StepsProgress from './StepsProgress/StepsProgress';
import events from './rows.json';
import { eventWithTime } from '@rrweb/types';
import RRWebPlayer from 'pages/SessionRecording/RRWebPlayer';
const homeInterval = 30 * 60 * 1000;
@@ -168,6 +171,37 @@ export default function Home(): JSX.Element {
false,
);
const playerContainerRef = useRef(null);
// const [player, setPlayer] = useState<init | null>(null);
const [parsedEvents, setParsedEvents] = useState<eventWithTime[]>([]);
useEffect(() => {
if (events.rows.length > 0) {
// Initialize the rrweb player with events
const parsedEvents = events.rows.map((event) => {
return JSON.parse(event.data.body);
});
setParsedEvents(parsedEvents);
// const replayPlayer = new init({
// target: (playerContainerRef.current as unknown) as HTMLElement,
// props: {
// events: parsedEvents, // Pass the captured events to the player
// speed: 1, // Normal speed (can be adjusted)
// showDebug: false, // Optionally show debug info
// },
// });
// // Save the player instance for future use if needed
// setPlayer(replayPlayer);
// // Cleanup on unmount
// return () => {
// replayPlayer.destroy();
// };
}
}, [events]); // Re-run effect when events change
const processUserPreferences = (userPreferences: UserPreference[]): void => {
const checklistSkipped = Boolean(
userPreferences?.find(
@@ -311,6 +345,9 @@ export default function Home(): JSX.Element {
return (
<div className="home-container">
<p>hello world</p>
<RRWebPlayer events={parsedEvents} options={{ autoPlay: false }} />
{/*
<div className="sticky-header">
{showBanner && (
<div className="home-container-banner">
@@ -378,7 +415,6 @@ export default function Home(): JSX.Element {
}
/>
</div>
<div className="home-content">
<div className="home-left-content">
<DataSourceInfo
@@ -764,7 +800,7 @@ export default function Home(): JSX.Element {
</>
)}
</div>
</div>
</div> */}
</div>
);
}

File diff suppressed because one or more lines are too long

View File

@@ -774,13 +774,6 @@ function MultiIngestionSettings(): JSX.Element {
),
children: (
<div className="ingestion-key-info-container">
<Row>
<Col span={6}> ID </Col>
<Col span={12}>
<Typography.Text>{APIKey.id}</Typography.Text>
</Col>
</Row>
<Row>
<Col span={6}> Created on </Col>
<Col span={12}>

View File

@@ -14,7 +14,4 @@ export const ContentWrapper = styled(Row)`
export const Wrapper = styled.div`
padding-bottom: 4rem;
padding-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
`;

View File

@@ -0,0 +1,14 @@
import { Card } from 'antd';
import styled from 'styled-components';
export const CardStyled = styled(Card)`
border: none !important;
position: relative;
margin-bottom: 16px;
.ant-card-body {
height: 200px;
min-height: 200px;
padding: 0 16px 16px 16px;
font-family: 'Geist Mono';
}
`;

View File

@@ -1,25 +0,0 @@
.logs-frequency-chart-container {
height: 200px;
min-height: 200px;
border-bottom: 1px solid #262626;
.ant-card-body {
height: 200px;
min-height: 200px;
padding: 0 16px 16px 16px;
font-family: 'Geist Mono';
}
.logs-frequency-chart-loading {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
.lightMode {
.logs-frequency-chart-container {
border-bottom: 1px solid var(--bg-vanilla-300);
}
}

View File

@@ -1,5 +1,3 @@
import './LogsExplorerChart.styles.scss';
import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import { QueryParams } from 'constants/query';
@@ -17,6 +15,7 @@ import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { LogsExplorerChartProps } from './LogsExplorerChart.interfaces';
import { CardStyled } from './LogsExplorerChart.styled';
import { getColorsForSeverityLabels } from './utils';
function LogsExplorerChart({
@@ -101,11 +100,9 @@ function LogsExplorerChart({
);
return (
<div className={`${className} logs-frequency-chart-container`}>
<CardStyled className={className}>
{isLoading ? (
<div className="logs-frequency-chart-loading">
<Spinner size="default" height="100%" />
</div>
<Spinner size="default" height="100%" />
) : (
<Graph
name="logsExplorerChart"
@@ -118,7 +115,7 @@ function LogsExplorerChart({
maxTime={chartMaxTime}
/>
)}
</div>
</CardStyled>
);
}

View File

@@ -1,263 +0,0 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable react-hooks/exhaustive-deps */
import { ColumnDef, DataTable, Row } from '@signozhq/table';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import LogStateIndicator from 'components/Logs/LogStateIndicator/LogStateIndicator';
import { getLogIndicatorTypeForTable } from 'components/Logs/LogStateIndicator/utils';
import { useTableView } from 'components/Logs/TableView/useTableView';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import { FontSize } from 'container/OptionsMenu/types';
import dayjs from 'dayjs';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import useDragColumns from 'hooks/useDragColumns';
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { isEmpty, isEqual } from 'lodash-es';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { ILog } from 'types/api/logs/log';
interface ColumnViewProps {
logs: ILog[];
onLoadMore: () => void;
selectedFields: any[];
isLoading: boolean;
isFetching: boolean;
isFrequencyChartVisible: boolean;
options: {
maxLinesPerRow: number;
fontSize: FontSize;
};
}
function ColumnView({
logs,
onLoadMore,
selectedFields,
isLoading,
isFetching,
isFrequencyChartVisible,
options,
}: ColumnViewProps): JSX.Element {
const {
activeLog,
onSetActiveLog: handleSetActiveLog,
onClearActiveLog: handleClearActiveLog,
onAddToQuery: handleAddToQuery,
onGroupByAttribute: handleGroupByAttribute,
} = useActiveLog();
const { queryData: activeLogId } = useUrlQueryData<string | null>(
QueryParams.activeLogId,
null,
);
const scrollToIndexRef = useRef<
| ((
rowIndex: number,
options?: { align?: 'start' | 'center' | 'end' },
) => void)
| undefined
>();
const { timezone } = useTimezone();
useEffect(() => {
if (activeLogId) {
const log = logs.find(({ id }) => id === activeLogId);
if (log) {
handleSetActiveLog(log);
}
}
}, [activeLogId, logs, handleSetActiveLog]);
const tableViewProps = {
logs,
fields: selectedFields,
linesPerRow: options.maxLinesPerRow as number,
fontSize: options.fontSize as FontSize,
appendTo: 'end' as const,
activeLogIndex: 0,
};
const { dataSource, columns } = useTableView({
...tableViewProps,
onClickExpand: handleSetActiveLog,
onOpenLogsContext: handleClearActiveLog,
});
const { draggedColumns, onColumnOrderChange } = useDragColumns<
Record<string, unknown>
>(LOCALSTORAGE.LOGS_LIST_COLUMNS);
const tableColumns = useMemo(
() => getDraggedColumns<Record<string, unknown>>(columns, draggedColumns),
[columns, draggedColumns],
);
const scrollToLog = useCallback(
(logId: string): void => {
const logIndex = logs.findIndex((log) => log.id === logId);
if (logIndex !== -1 && scrollToIndexRef.current) {
scrollToIndexRef.current(logIndex, { align: 'center' });
}
},
[logs],
);
useEffect(() => {
if (activeLogId) {
scrollToLog(activeLogId);
}
}, [activeLogId]);
const args = {
columns,
tableId: 'virtualized-infinite-reorder-resize',
enableSorting: false,
enableFiltering: false,
enableGlobalFilter: false,
enableColumnReordering: true,
enableColumnResizing: true,
enableColumnPinning: false,
enableRowSelection: false,
enablePagination: false,
showHeaders: true,
defaultColumnWidth: 180,
minColumnWidth: 80,
maxColumnWidth: 480,
// Virtualization + Infinite Scroll
enableVirtualization: true,
estimateRowSize: 56,
overscan: 50,
rowHeight: 56,
enableInfiniteScroll: true,
enableScrollRestoration: false,
fixedHeight: isFrequencyChartVisible ? 560 : 760,
enableDynamicRowHeight: true,
};
const selectedColumns = useMemo(
() =>
tableColumns.map((field) => ({
id: field.key?.toString().toLowerCase().replace(/\./g, '_'), // IMP - Replace dots with underscores as reordering does not work well for accessorKey with dots
// accessorKey: field.name,
accessorFn: (row: Record<string, string>): string =>
row[field.key as string] as string,
header: field.title as string,
// eslint-disable-next-line sonarjs/no-duplicate-string
size: field.key === 'state-indicator' ? 4 : 180,
minSize: field.key === 'state-indicator' ? 4 : 120,
maxSize: field.key === 'state-indicator' ? 4 : Number.MAX_SAFE_INTEGER,
disableReorder: field.key === 'state-indicator',
disableDropBefore: field.key === 'state-indicator',
disableResizing: field.key === 'state-indicator',
// eslint-disable-next-line react/no-unstable-nested-components
cell: ({
row,
getValue,
}: {
row: Row<Record<string, string>>;
getValue: () => string | JSX.Element;
}): string | JSX.Element => {
if (field.key === 'state-indicator') {
const type = getLogIndicatorTypeForTable(row.original);
const fontSize = options.fontSize as FontSize;
return <LogStateIndicator type={type} fontSize={fontSize} />;
}
const isTimestamp = field.key === 'timestamp';
const cellContent = getValue();
if (isTimestamp) {
const formattedTimestamp = dayjs(cellContent as string).tz(
timezone.value,
);
return (
<div className="table-cell-content">
{formattedTimestamp.format(DATE_TIME_FORMATS.ISO_DATETIME_MS)}
</div>
);
}
return (
<div
className={`table-cell-content ${
row.original.id === activeLog?.id ? 'active-log' : ''
}`}
>
{cellContent}
</div>
);
},
})),
[tableColumns, options.fontSize, activeLog?.id],
);
const handleColumnOrderChange = (newColumns: ColumnDef<any>[]): void => {
if (isEmpty(newColumns) || isEqual(newColumns, selectedColumns)) return;
const formattedColumns = newColumns.map((column) => ({
id: column.id,
header: column.header,
size: column.size,
minSize: column.minSize,
maxSize: column.maxSize,
key: column.id,
title: column.header as string,
dataIndex: column.id,
}));
onColumnOrderChange(formattedColumns);
};
const handleRowClick = (row: Row<Record<string, unknown>>): void => {
const currentLog = logs.find(({ id }) => id === row.original.id);
handleSetActiveLog(currentLog as ILog);
};
return (
<div
className={`logs-list-table-view-container ${
options.fontSize as FontSize
} max-lines-${options.maxLinesPerRow as number}`}
data-max-lines-per-row={options.maxLinesPerRow}
data-font-size={options.fontSize}
>
<DataTable
// eslint-disable-next-line react/jsx-props-no-spreading
{...args}
columns={selectedColumns as ColumnDef<Record<string, string>, unknown>[]}
data={dataSource}
hasMore
onLoadMore={onLoadMore}
loadingMore={isLoading || isFetching}
onColumnOrderChange={handleColumnOrderChange}
onRowClick={handleRowClick}
scrollToIndexRef={scrollToIndexRef}
/>
{activeLog && (
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={handleClearActiveLog}
onAddToQuery={handleAddToQuery}
onClickActionItem={handleAddToQuery}
onGroupByAttribute={handleGroupByAttribute}
/>
)}
</div>
);
}
export default ColumnView;

View File

@@ -11,5 +11,4 @@ export type LogsExplorerListProps = {
isError: boolean;
error?: Error | APIError;
isFilterApplied: boolean;
isFrequencyChartVisible: boolean;
};

View File

@@ -9,364 +9,4 @@
letter-spacing: -0.005em;
text-align: left;
min-height: 500px;
.logs-list-table-view-container {
.data-table-container {
border: none !important;
}
table {
thead {
tr {
th {
font-family: 'Space Mono', monospace;
font-size: 12px;
color: white !important;
.cursor-col-resize {
width: 3px !important;
cursor: col-resize !important;
opacity: 0.5 !important;
background-color: var(--bg-ink-500) !important;
border: 1px solid var(--bg-ink-500) !important;
&:hover {
opacity: 1 !important;
}
}
}
td {
font-family: 'Space Mono', monospace;
font-size: 12px;
}
border-bottom: 1px solid var(--bg-slate-400) !important;
}
border-bottom: 1px solid var(--bg-slate-400) !important;
background-color: var(--bg-ink-400) !important;
tr {
&:hover {
background-color: var(--bg-ink-400) !important;
}
}
}
tbody {
tr {
td {
font-family: 'Space Mono', monospace;
font-size: 12px;
padding: 4px !important;
white-space: pre-wrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
&:hover {
background-color: var(--bg-slate-500) !important;
}
border-bottom: 1px solid var(--bg-slate-400) !important;
}
tr:has(.active-log) {
background-color: rgba(
78,
116,
248,
0.5
) !important; // same as bg-robin-500
color: var(--text-vanilla-100) !important;
}
}
thead {
z-index: 0 !important;
}
.log-state-indicator {
padding-left: 0 !important;
.line {
margin: 0 0 !important;
}
}
}
.sticky-header-table-container {
&::-webkit-scrollbar {
width: 0.4rem;
height: 0.4rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
}
&.small {
tbody {
tr {
td {
.table-cell-content {
font-size: 12px !important;
}
}
}
}
}
&.medium {
tbody {
tr {
td {
.table-cell-content {
font-size: 14px !important;
}
}
}
}
}
&.large {
tbody {
tr {
td {
.table-cell-content {
font-size: 16px !important;
}
}
}
}
}
&.max-lines-1 {
tbody {
tr {
td {
.table-cell-content {
font-size: 12px !important;
}
}
}
}
}
&[data-max-lines-per-row='1'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
&[data-max-lines-per-row='2'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
&[data-max-lines-per-row='3'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
&[data-max-lines-per-row='4'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
&[data-max-lines-per-row='5'] {
tbody {
tr {
td {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 5;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
&[data-max-lines-per-row='6'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 6;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
&[data-max-lines-per-row='7'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 7;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
&[data-max-lines-per-row='8'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 8;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
&[data-max-lines-per-row='9'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 9;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
&[data-max-lines-per-row='10'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 10;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
}
}
.lightMode {
.logs-list-view-container {
.logs-list-table-view-container {
table {
thead {
tr {
th {
color: var(--text-ink-500) !important;
}
border-bottom: 1px solid var(--bg-vanilla-300) !important;
}
border-bottom: 1px solid var(--bg-vanilla-300) !important;
background-color: var(--bg-vanilla-100) !important;
tr {
&:hover {
background-color: var(--bg-vanilla-300) !important;
}
}
}
tbody {
tr {
&:hover {
background-color: var(--bg-vanilla-300) !important;
}
border-bottom: 1px solid var(--bg-vanilla-300) !important;
}
}
}
.sticky-header-table-container {
&::-webkit-scrollbar-thumb {
background: var(--bg-vanilla-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-vanilla-100);
}
}
}
}
}

View File

@@ -90,7 +90,6 @@ function LogsExplorerList({
});
}
}, [isLoading, isFetching, isError, logs.length]);
const getItemContent = useCallback(
(_: number, log: ILog): JSX.Element => {
if (options.format === 'raw') {

View File

@@ -9,7 +9,7 @@
display: flex;
flex-direction: column;
flex: 1;
padding-bottom: 10px;
padding-bottom: 60px;
.views-tabs-container {
padding: 8px 16px;
@@ -216,19 +216,15 @@
background-color: var(--bg-ink-500);
}
.logs-frequency-chart-container {
padding: 0px 8px;
.logs-frequency-chart {
.ant-card-body {
height: 140px;
min-height: 140px;
padding: 0 16px 22px 16px;
font-family: 'Geist Mono';
}
margin-bottom: 0px;
.logs-histogram {
.ant-card-body {
height: 140px;
min-height: 140px;
padding: 0 16px 22px 16px;
font-family: 'Geist Mono';
}
margin-bottom: 0px;
}
}

View File

@@ -2,8 +2,6 @@
import './LogsExplorerViews.styles.scss';
import { Button, Switch, Typography } from 'antd';
import getFromLocalstorage from 'api/browser/localstorage/get';
import setToLocalstorage from 'api/browser/localstorage/set';
import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats';
import logEvent from 'api/common/logEvent';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
@@ -105,13 +103,7 @@ function LogsExplorerViewsContainer({
}): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const dispatch = useDispatch();
const [showFrequencyChart, setShowFrequencyChart] = useState(false);
useEffect(() => {
const frequencyChart = getFromLocalstorage(LOCALSTORAGE.SHOW_FREQUENCY_CHART);
setShowFrequencyChart(frequencyChart === 'true');
}, []);
const [showFrequencyChart, setShowFrequencyChart] = useState(true);
// this is to respect the panel type present in the URL rather than defaulting it to list always.
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
@@ -680,18 +672,6 @@ function LogsExplorerViewsContainer({
[logs, timezone.value],
);
const handleToggleFrequencyChart = useCallback(() => {
const newShowFrequencyChart = !showFrequencyChart;
// store the value in local storage
setToLocalstorage(
LOCALSTORAGE.SHOW_FREQUENCY_CHART,
newShowFrequencyChart?.toString() || 'false',
);
setShowFrequencyChart(newShowFrequencyChart);
}, [showFrequencyChart]);
return (
<div className="logs-explorer-views-container">
<div className="logs-explorer-views-types">
@@ -705,7 +685,7 @@ function LogsExplorerViewsContainer({
size="small"
checked={showFrequencyChart}
defaultChecked
onChange={handleToggleFrequencyChart}
onChange={(): void => setShowFrequencyChart(!showFrequencyChart)}
/>
</div>
)}
@@ -781,14 +761,12 @@ function LogsExplorerViewsContainer({
</div>
{selectedPanelType === PANEL_TYPES.LIST && showFrequencyChart && (
<div className="logs-frequency-chart-container">
<LogsExplorerChart
className="logs-frequency-chart"
isLoading={isFetchingListChartData || isLoadingListChartData}
data={chartData}
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
/>
</div>
<LogsExplorerChart
className="logs-histogram"
isLoading={isFetchingListChartData || isLoadingListChartData}
data={chartData}
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
/>
)}
<div className="logs-explorer-views-type-content">
@@ -799,12 +777,12 @@ function LogsExplorerViewsContainer({
currentStagedQueryData={listQuery}
logs={logs}
onEndReached={handleEndReached}
isFrequencyChartVisible={showFrequencyChart}
isError={isError}
error={error as APIError}
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
/>
)}
{selectedPanelType === PANEL_TYPES.TIME_SERIES && (
<TimeSeriesView
isLoading={isLoading || isFetching}

View File

@@ -65,6 +65,17 @@ const useOptionsMenu = ({
const [isFocused, setIsFocused] = useState<boolean>(false);
const debouncedSearchText = useDebounce(searchText, 300);
// const initialQueryParams = useMemo(
// () => ({
// searchText: '',
// aggregateAttribute: '',
// tagType: undefined,
// dataSource,
// aggregateOperator,
// }),
// [dataSource, aggregateOperator],
// );
const initialQueryParamsV5: QueryKeyRequestProps = useMemo(
() => ({
signal: dataSource,
@@ -78,6 +89,22 @@ const useOptionsMenu = ({
redirectWithQuery: redirectWithOptionsData,
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
// const initialQueries = useMemo(
// () =>
// initialOptions?.selectColumns?.map((column) => ({
// queryKey: column,
// queryFn: (): Promise<
// SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
// > =>
// getAggregateKeys({
// ...initialQueryParams,
// searchText: column,
// }),
// enabled: !!column && !optionsQuery,
// })) || [],
// [initialOptions?.selectColumns, initialQueryParams, optionsQuery],
// );
const initialQueriesV5 = useMemo(
() =>
initialOptions?.selectColumns?.map((column) => ({

View File

@@ -9,6 +9,8 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import APIError from 'types/api/error';
import { Container } from '../styles';
function AddDomain({ refetch }: Props): JSX.Element {
const { t } = useTranslation(['common', 'organizationsettings']);
const [isAddDomains, setIsDomain] = useState(false);
@@ -40,7 +42,7 @@ function AddDomain({ refetch }: Props): JSX.Element {
return (
<>
<div className="auth-domains-title-container">
<Container>
<Typography.Title level={3}>
{t('authenticated_domains', {
ns: 'organizationsettings',
@@ -53,11 +55,10 @@ function AddDomain({ refetch }: Props): JSX.Element {
>
{t('add_domain', { ns: 'organizationsettings' })}
</Button>
</div>
</Container>
<Modal
centered
title="Add Domain"
className="add-domain-modal"
footer={null}
open={isAddDomains}
destroyOnClose

View File

@@ -244,7 +244,7 @@ function AuthDomains(): JSX.Element {
/>
</Modal>
<div className="auth-domains-container">
<Space direction="vertical" size="middle">
<AddDomain refetch={refetch} />
<ResizeTable
@@ -255,7 +255,7 @@ function AuthDomains(): JSX.Element {
rowKey={(record: AuthDomain): string => record.name + v4()}
bordered
/>
</div>
</Space>
</>
);
}

View File

@@ -5,7 +5,6 @@ export const Container = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
`;
export const ColumnWithTooltip = styled(Row)`

View File

@@ -152,7 +152,6 @@ function InviteUserModal(props: InviteUserModalProps): JSX.Element {
onCancel={(): void => toggleModal(false)}
centered
data-testid="invite-team-members-modal"
className="invite-user-modal"
destroyOnClose
footer={[
<Button key="back" onClick={(): void => toggleModal(false)} type="default">

View File

@@ -157,7 +157,6 @@ function UserFunction({
</Space>
<Modal
title="Edit member details"
className="edit-member-details-modal"
open={isModalVisible}
onOk={(): void => onModalToggleHandler(setIsModalVisible, false)}
onCancel={(): void => onModalToggleHandler(setIsModalVisible, false)}
@@ -286,7 +285,7 @@ function Members(): JSX.Element {
];
return (
<div className="members-container">
<Space direction="vertical" size="middle">
<Typography.Title level={3}>
Members{' '}
{!isLoading && dataSource && (
@@ -301,7 +300,7 @@ function Members(): JSX.Element {
loading={status === 'loading'}
bordered
/>
</div>
</Space>
);
}

View File

@@ -1,40 +1,18 @@
.organization-settings-container {
display: flex;
flex-direction: column;
gap: 24px;
margin: 16px;
gap: 16px;
margin: 16px auto;
padding: 16px;
width: 90%;
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
border-radius: 3px;
}
.pending-invites-container,
.members-container,
.auth-domains-container {
display: flex;
flex-direction: column;
gap: 8px;
h3 {
margin-bottom: 0px;
}
}
.auth-domains-title-container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 8px;
h3 {
margin-bottom: 0px;
}
}
.lightMode {
.organization-settings-container {
border: 1px solid var(--bg-vanilla-300);

View File

@@ -180,7 +180,7 @@ function PendingInvitesContainer(): JSX.Element {
];
return (
<div className="pending-invites-container-wrapper">
<div>
<InviteUserModal
form={form}
isInviteTeamMemberModalOpen={isInviteTeamMemberModalOpen}
@@ -189,7 +189,7 @@ function PendingInvitesContainer(): JSX.Element {
shouldCallApi
/>
<div className="pending-invites-container">
<Space direction="vertical" size="middle">
<TitleWrapper>
<Typography.Title level={3}>
{t('pending_invites')}
@@ -218,7 +218,7 @@ function PendingInvitesContainer(): JSX.Element {
loading={getPendingInvitesResponse.status === 'loading'}
bordered
/>
</div>
</Space>
</div>
);
}

View File

@@ -1,6 +1,4 @@
import './OrganizationSettings.styles.scss';
import { Space } from 'antd';
import { Divider, Space } from 'antd';
import { useAppContext } from 'providers/App/App';
import AuthDomains from './AuthDomains';
@@ -26,6 +24,7 @@ function OrganizationSettings(): JSX.Element {
<PendingInvitesContainer />
<Members />
<Divider />
<AuthDomains />
</div>
);

View File

@@ -42,11 +42,11 @@
gap: 8px;
&.active-tab {
background-color: var(--bg-robin-500);
border-bottom: 1px solid var(--bg-robin-500);
background-color: var(--bg-ink-500);
border-bottom: 1px solid var(--bg-ink-500);
&:hover {
background-color: var(--bg-robin-500) !important;
background-color: var(--bg-ink-500) !important;
}
}

View File

@@ -31,6 +31,7 @@ import {
Unplug,
User,
UserPlus,
Video,
} from 'lucide-react';
import { SecondaryMenuItemKey, SidebarItem } from './sideNav.types';
@@ -103,7 +104,6 @@ const menuItems: SidebarItem[] = [
icon: <HardDrive size={16} />,
itemKey: 'services',
},
{
key: ROUTES.LOGS,
label: 'Logs',
@@ -274,6 +274,13 @@ export const defaultMoreMenuItems: SidebarItem[] = [
isBeta: true,
itemKey: 'meter-explorer',
},
{
key: ROUTES.SESSION_RECORDINGS,
label: 'Session Recordings',
icon: <Video size={16} />,
isEnabled: true,
itemKey: 'session-recordings',
},
{
key: ROUTES.MESSAGING_QUEUES_OVERVIEW,
label: 'Messaging Queues',

View File

@@ -237,6 +237,8 @@ export const routesToSkip = [
ROUTES.METER,
ROUTES.METER_EXPLORER_VIEWS,
ROUTES.SOMETHING_WENT_WRONG,
ROUTES.SESSION_RECORDINGS,
ROUTES.SESSION_RECORDINGS_DETAIL,
];
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];

View File

@@ -12,7 +12,6 @@ export type UseCopyLogLink = {
isLogsExplorerPage: boolean;
activeLogId: string | null;
onLogCopy: MouseEventHandler<HTMLElement>;
onClearActiveLog: () => void;
};
export type UseActiveLog = {

View File

@@ -1,7 +1,6 @@
import { toast } from '@signozhq/sonner';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import useUrlQueryData from 'hooks/useUrlQueryData';
import {
@@ -22,10 +21,9 @@ import { UseCopyLogLink } from './types';
export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
const urlQuery = useUrlQuery();
const { pathname, search } = useLocation();
const { pathname } = useLocation();
const [, setCopy] = useCopyToClipboard();
const { safeNavigate } = useSafeNavigate();
const { notifications } = useNotifications();
const { queryData: activeLogId } = useUrlQueryData<string | null>(
QueryParams.activeLogId,
@@ -60,19 +58,13 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
setCopy(link);
toast.success('Copied to clipboard', { position: 'top-right' });
notifications.success({
message: 'Copied to clipboard',
});
},
[logId, urlQuery, minTime, maxTime, pathname, setCopy],
[logId, urlQuery, minTime, maxTime, pathname, setCopy, notifications],
);
const onClearActiveLog = useCallback(() => {
const currentUrlQuery = new URLSearchParams(search);
currentUrlQuery.delete(QueryParams.activeLogId);
const newUrl = `${pathname}?${currentUrlQuery.toString()}`;
safeNavigate(newUrl);
}, [pathname, search, safeNavigate]);
useEffect(() => {
if (!isActiveLog) return;
@@ -89,6 +81,5 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
isLogsExplorerPage,
activeLogId,
onLogCopy,
onClearActiveLog,
};
};

View File

@@ -40,13 +40,6 @@ const useDragColumns = <T>(storageKey: LOCALSTORAGE): UseDragColumns<T> => {
[handleRedirectWithDraggedColumns],
);
const onColumnOrderChange = useCallback(
(newColumns: ColumnsType<T>): void => {
handleRedirectWithDraggedColumns(newColumns);
},
[handleRedirectWithDraggedColumns],
);
const redirectWithNewDraggedColumns = useCallback(
async (localStorageColumns: string) => {
let nextDraggedColumns: ColumnsType<T> = [];
@@ -76,7 +69,6 @@ const useDragColumns = <T>(storageKey: LOCALSTORAGE): UseDragColumns<T> => {
return {
draggedColumns,
onDragColumns,
onColumnOrderChange,
};
};

View File

@@ -7,5 +7,4 @@ export type UseDragColumns<T> = {
fromIndex: number,
toIndex: number,
) => void;
onColumnOrderChange: (newColumns: ColumnsType<T>) => void;
};

View File

@@ -105,6 +105,7 @@ const logsQueryServerRequest = (): void =>
describe('Logs Explorer Tests', () => {
test('Logs Explorer default view test without data', async () => {
const {
getByText,
getByRole,
queryByText,
getByTestId,
@@ -122,10 +123,13 @@ describe('Logs Explorer Tests', () => {
</MemoryRouter>,
);
// by default is hidden, toggle the chart and check it's visibility
// check the presence of frequency chart content
expect(getByText(frequencyChartContent)).toBeInTheDocument();
// toggle the chart and check it gets removed from the DOM
const histogramToggle = getByRole('switch');
fireEvent.click(histogramToggle);
expect(queryByText(frequencyChartContent)).toBeInTheDocument();
expect(queryByText(frequencyChartContent)).not.toBeInTheDocument();
// check the presence of search bar and query builder and absence of clickhouse
const searchView = getByTestId('search-view');
@@ -273,10 +277,10 @@ describe('Logs Explorer Tests', () => {
const histogramToggle = getByRole('switch');
expect(histogramToggle).toBeInTheDocument();
expect(histogramToggle).toBeChecked();
expect(queryByText(frequencyChartContent)).toBeInTheDocument();
// toggle the chart and check it gets removed from the DOM
await fireEvent.click(histogramToggle);
expect(histogramToggle).not.toBeChecked();
expect(queryByText(frequencyChartContent)).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,82 @@
# Session Recordings Page
A minimal, focused page that displays session recordings with just session names and play buttons.
## Features
- **Simple Session List**: Clean table showing only session names and play buttons
- **Direct Access**: Click play button or row to open session recording
- **Minimal UI**: No filters, search, or extra information - just the essentials
- **Responsive Design**: Adapts to different screen sizes
- **Dark/Light Mode Support**: Follows the app's theme system
## Components
### Main Component
- `index.tsx` - Main session recordings page component
### Supporting Files
- `types.ts` - TypeScript interfaces for session recording data
- `styles.scss` - Minimal styling with theme support
- `README.md` - This documentation file
## Data Structure
Each session recording includes:
- Basic identification (ID, session ID)
- User information (name, user agent)
- Timing details (start time, duration)
- Geographic data (country, city)
- Technical details (device, browser, OS)
- Status information (completion status, error flags)
- Recording URL for playback
## Table Columns
1. **Session Name** - Session identifier
2. **Actions** - Play button to open session recording
## Usage
The page automatically loads with mock data for demonstration. In production, replace the mock data with actual API calls to fetch session recordings.
### Table Interaction
- Click on any row to open the session recording
- Use the play button in the Actions column for quick access
- Sort by session name by clicking the column header
- Navigate through pages using the pagination controls
## Styling
The page uses CSS custom properties for theming:
- Dark mode: Uses `--bg-ink-*` and `--bg-slate-*` color variables
- Light mode: Uses `--bg-vanilla-*` color variables
- Accent colors: Uses `--bg-sakura-*` for primary actions and highlights
## Responsive Behavior
- **Desktop**: Clean table layout with proper spacing
- **Tablet**: Responsive table with maintained readability
- **Mobile**: Single-column layout with touch-friendly buttons
## Design Philosophy
The UI follows an extremely minimalist approach:
- Only essential information displayed
- No visual clutter or unnecessary features
- Focus on quick access to session recordings
- Clean, readable table layout
- Consistent with the app's design system
## Future Enhancements
- Session count display
- Basic sorting options
- Export functionality
- Real-time updates for active sessions

View File

@@ -0,0 +1,80 @@
import React, { useEffect, useRef } from 'react';
import Player, { RRwebPlayerOptions } from 'rrweb-player';
import { eventWithTime } from '@rrweb/types';
import 'rrweb-player/dist/style.css'; // Import the styles for the player
interface RRWebPlayerProps {
events: eventWithTime[];
options?: Partial<RRwebPlayerOptions['props']>;
className?: string;
style?: React.CSSProperties;
}
const RRWebPlayer: React.FC<RRWebPlayerProps> = ({
events,
options = {},
className = '',
style = {},
}) => {
const playerRef = useRef<HTMLDivElement>(null);
const playerInstanceRef = useRef<any>(null);
useEffect(() => {
if (!playerRef.current || !events || events.length === 0) {
return;
}
// Clean up previous instance if it exists
if (playerInstanceRef.current) {
// Remove the previous player element
if (playerRef.current) {
playerRef.current.innerHTML = '';
}
}
// Create new player instance using the imported Player
playerInstanceRef.current = new Player({
target: playerRef.current as HTMLElement,
props: {
events,
...options,
},
});
// Cleanup function
return () => {
if (playerInstanceRef.current && playerRef.current) {
playerRef.current.innerHTML = '';
playerInstanceRef.current = null;
}
};
}, [events, options]);
// Cleanup on unmount
useEffect(() => {
return () => {
if (playerInstanceRef.current && playerRef.current) {
playerRef.current.innerHTML = '';
playerInstanceRef.current = null;
}
};
}, []);
if (!events || events.length === 0) {
return (
<div className={`rrweb-player-empty ${className}`} style={style}>
<p>No session events available</p>
</div>
);
}
return (
<div
ref={playerRef}
className={`rrweb-player-container ${className}`}
style={style}
/>
);
};
export default RRWebPlayer;

View File

@@ -0,0 +1,129 @@
import * as Sentry from '@sentry/react';
import { Button, Typography } from 'antd';
import { ArrowLeft } from 'lucide-react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { DataSource } from 'types/common/queryBuilder';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { SessionRecording } from '../types';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { PANEL_TYPES, initialQueriesMap } from 'constants/queryBuilder';
import RRWebPlayer from '../RRWebPlayer';
import React from 'react';
import './styles.scss';
import ROUTES from 'constants/routes';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
const { Title, Text, Paragraph } = Typography;
export default function SessionDetail(): JSX.Element {
const { stagedQuery } = useQueryBuilder();
const { sessionId } = useParams<{ sessionId: string }>();
const history = useHistory();
// Fetch session-related logs data using useGetQueryRange
const {
data: sessionLogsData,
isLoading: isSessionLogsLoading,
} = useGetQueryRange(
{
query: stagedQuery || initialQueriesMap.logs,
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: '3d',
params: {
dataSource: DataSource.LOGS,
},
formatForWeb: false,
},
ENTITY_VERSION_V5,
{
queryKey: ['sessionLogs', sessionId],
enabled: !!sessionId && !!stagedQuery,
},
);
console.log({ sessionLogsData });
// Extract body fields from session logs data
const sessionEvents = React.useMemo(() => {
if (!sessionLogsData?.payload?.data?.newResult?.data?.result?.[0]?.list) {
return [];
}
return sessionLogsData.payload.data.newResult.data.result[0].list
.map((row: any) => {
// Try to extract body field from different possible locations
const body =
row.data?.body || row.data?.message || row.data?.log || row.data;
// If body is a string, try to parse it as JSON
if (typeof body === 'string') {
try {
return JSON.parse(body);
} catch {
// If parsing fails, return the string as is
return body;
}
}
return body;
})
.filter(Boolean); // Remove any undefined/null values
}, [sessionLogsData]);
const handleBack = (): void => {
history.push(ROUTES.SESSION_RECORDINGS);
};
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="session-detail-page">
<div className="page-header">
<div className="header-content">
<Button
type="text"
icon={<ArrowLeft size={16} />}
onClick={handleBack}
className="back-button"
>
Back to Sessions
</Button>
<Title level={2} className="page-title">
Session Recording: {sessionId}
</Title>
<Text type="secondary" className="page-description">
Detailed view of session recording and metadata
</Text>
</div>
</div>
<div className="page-content">
{/* Display RRWebPlayer with session events */}
{isSessionLogsLoading ? (
<div>Loading session logs...</div>
) : sessionEvents.length > 0 ? (
<div>
<h3>Session Recording Player</h3>
<div style={{ marginBottom: '16px' }}>
<strong>Total Events: {sessionEvents.length}</strong>
</div>
<RRWebPlayer
events={sessionEvents}
options={{
autoPlay: false,
}}
/>
</div>
) : (
<div>No session events available</div>
)}
</div>
</div>
</Sentry.ErrorBoundary>
);
}

View File

@@ -0,0 +1,306 @@
.session-detail-page {
height: 100%;
background: var(--bg-ink-400);
padding: 0;
.page-header {
background: var(--bg-ink-500);
border-bottom: 1px solid var(--bg-slate-400);
padding: 24px 24px 16px;
margin-bottom: 0;
.header-content {
.back-button {
margin-bottom: 16px;
color: var(--bg-vanilla-400);
padding: 0;
height: auto;
font-size: 14px;
&:hover {
color: var(--bg-vanilla-100);
}
.anticon {
margin-right: 8px;
}
}
.page-title {
margin: 0 0 8px 0;
color: var(--bg-vanilla-100);
font-weight: 600;
font-size: 24px;
line-height: 32px;
}
.page-description {
font-size: 14px;
line-height: 20px;
color: var(--bg-vanilla-400);
margin: 0;
}
}
}
.page-content {
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
.content-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 16px;
.overview-card,
.user-card,
.device-card,
.player-card,
.logs-card {
background: var(--bg-ink-500);
border: 1px solid var(--bg-slate-400);
border-radius: 6px;
.ant-card-head {
background: var(--bg-ink-400);
border-bottom: 1px solid var(--bg-slate-400);
.ant-card-head-title {
color: var(--bg-vanilla-100);
font-weight: 600;
font-size: 16px;
line-height: 24px;
}
}
.ant-card-body {
padding: 16px;
}
.ant-descriptions {
.ant-descriptions-item-label {
color: var(--bg-vanilla-400);
font-weight: 500;
font-size: 12px;
line-height: 16px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.ant-descriptions-item-content {
color: var(--bg-vanilla-100);
font-size: 14px;
line-height: 20px;
}
}
.user-agent-text {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
}
.logs-content {
.logs-count {
display: block;
margin-top: 8px;
font-size: 14px;
}
.ant-typography {
margin-bottom: 8px;
}
}
line-height: 16px;
word-break: break-all;
max-width: 100%;
display: block;
}
.ant-tag {
font-size: 12px;
line-height: 16px;
padding: 2px 8px;
border-radius: 4px;
}
.ant-space {
.anticon {
color: var(--bg-vanilla-400);
flex-shrink: 0;
}
}
}
.player-card {
grid-column: span 2;
.player-content {
.player-description {
color: var(--bg-vanilla-400);
font-size: 14px;
line-height: 20px;
margin-bottom: 24px;
text-align: center;
}
.player-actions {
display: flex;
justify-content: center;
.play-button-large {
background: var(--bg-sakura-500);
border-color: var(--bg-sakura-500);
color: var(--bg-ink-500);
height: 48px;
padding: 0 32px;
font-size: 16px;
font-weight: 500;
border-radius: 8px;
&:hover {
background: var(--bg-sakura-600);
border-color: var(--bg-sakura-600);
}
.anticon {
margin-right: 8px;
}
}
}
}
}
}
}
// Light mode overrides
.lightMode {
.session-detail-page {
background: var(--bg-vanilla-100);
.page-header {
background: var(--bg-vanilla-200);
border-bottom: 1px solid var(--bg-slate-300);
.header-content {
.back-button {
color: var(--bg-ink-400);
&:hover {
color: var(--bg-ink-500);
}
}
.page-title {
color: var(--bg-ink-500);
}
.page-description {
color: var(--bg-ink-400);
}
}
}
.page-content {
.content-grid {
.overview-card,
.user-card,
.device-card,
.player-card {
background: var(--bg-vanilla-200);
border: 1px solid var(--bg-slate-300);
.ant-card-head {
background: var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-slate-300);
.ant-card-head-title {
color: var(--bg-ink-500);
}
}
.ant-descriptions {
.ant-descriptions-item-label {
color: var(--bg-ink-400);
}
.ant-descriptions-item-content {
color: var(--bg-ink-500);
}
}
.ant-space {
.anticon {
color: var(--bg-ink-400);
}
}
}
.player-card {
.player-content {
.player-description {
color: var(--bg-ink-400);
}
}
}
}
}
}
}
// Responsive design
@media (max-width: 768px) {
.session-detail-page {
.page-content {
padding: 16px;
.content-grid {
grid-template-columns: 1fr;
gap: 12px;
.player-card {
grid-column: span 1;
}
}
}
}
}
@media (max-width: 480px) {
.session-detail-page {
.page-header {
padding: 16px 16px 12px;
.header-content {
.page-title {
font-size: 20px;
line-height: 28px;
}
}
}
.page-content {
padding: 12px;
.content-grid {
gap: 12px;
.overview-card,
.user-card,
.device-card,
.player-card {
.ant-card-body {
padding: 12px;
}
.ant-descriptions {
.ant-descriptions-item {
margin-bottom: 8px;
}
}
}
}
}
}
}

View File

@@ -0,0 +1,166 @@
import * as Sentry from '@sentry/react';
import { Button, Card, Typography } from 'antd';
import { PlayCircle } from 'lucide-react';
import { useQuery } from 'react-query';
import { useHistory } from 'react-router-dom';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { ResizeTable } from 'components/ResizeTable';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { DataSource } from 'types/common/queryBuilder';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { SessionRecording } from './types';
import { useMemo } from 'react';
import './styles.scss';
const { Title, Text } = Typography;
export default function SessionRecordings(): JSX.Element {
const history = useHistory();
// Use React Query to fetch session attributes
const { data: sessionAttributes, isLoading, error } = useQuery({
queryKey: ['sessionAttributes', 'rum.sessionId'],
queryFn: () =>
getAttributesValues({
aggregateOperator: 'noop',
dataSource: DataSource.LOGS,
aggregateAttribute: '',
attributeKey: 'rum.sessionId',
searchText: '',
filterAttributeKeyDataType: DataTypes.String,
tagType: 'resource',
}),
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
});
const getQueryParams = (sessionId: string) => {
const compositeQuery = `compositeQuery=%257B%2522queryType%2522%253A%2522builder%2522%252C%2522builder%2522%253A%257B%2522queryData%2522%253A%255B%257B%2522dataSource%2522%253A%2522logs%2522%252C%2522queryName%2522%253A%2522A%2522%252C%2522aggregateOperator%2522%253A%2522count%2522%252C%2522aggregateAttribute%2522%253A%257B%2522id%2522%253A%2522----%2522%252C%2522dataType%2522%253A%2522%2522%252C%2522key%2522%253A%2522%2522%252C%2522type%2522%253A%2522%2522%257D%252C%2522timeAggregation%2522%253A%2522rate%2522%252C%2522spaceAggregation%2522%253A%2522sum%2522%252C%2522filter%2522%253A%257B%2522expression%2522%253A%2522rum.sessionId%2520%253D%2520%27${sessionId}%27%2520%2522%257D%252C%2522aggregations%2522%253A%255B%257B%2522expression%2522%253A%2522count%28%29%2520%2522%257D%255D%252C%2522functions%2522%253A%255B%255D%252C%2522filters%2522%253A%257B%2522items%2522%253A%255B%255D%252C%2522op%2522%253A%2522AND%2522%257D%252C%2522expression%2522%253A%2522A%2522%252C%2522disabled%2522%253Afalse%252C%2522stepInterval%2522%253Anull%252C%2522having%2522%253A%257B%2522expression%2522%253A%2522%2522%257D%252C%2522limit%2522%253Anull%252C%2522orderBy%2522%253A%255B%255D%252C%2522groupBy%2522%253A%255B%255D%252C%2522legend%2522%253A%2522%2522%252C%2522reduceTo%2522%253A%2522avg%2522%252C%2522source%2522%253A%2522%2522%257D%255D%252C%2522queryFormulas%2522%253A%255B%255D%257D%252C%2522promql%2522%253A%255B%257B%2522name%2522%253A%2522A%2522%252C%2522query%2522%253A%2522%2522%252C%2522legend%2522%253A%2522%2522%252C%2522disabled%2522%253Afalse%257D%255D%252C%2522clickhouse_sql%2522%253A%255B%257B%2522name%2522%253A%2522A%2522%252C%2522legend%2522%253A%2522%2522%252C%2522disabled%2522%253Afalse%252C%2522query%2522%253A%2522%2522%257D%255D%252C%2522id%2522%253A%25226a102b3b-cb6b-4409-9e57-317f9ecb941b%2522%257D&options=%7B%22selectColumns%22%3A%5B%7B%22name%22%3A%22timestamp%22%2C%22signal%22%3A%22logs%22%2C%22fieldContext%22%3A%22log%22%2C%22fieldDataType%22%3A%22%22%2C%22isIndexed%22%3Afalse%7D%2C%7B%22name%22%3A%22body%22%2C%22signal%22%3A%22logs%22%2C%22fieldContext%22%3A%22log%22%2C%22fieldDataType%22%3A%22%22%2C%22isIndexed%22%3Afalse%7D%5D%2C%22maxLines%22%3A2%2C%22format%22%3A%22raw%22%2C%22fontSize%22%3A%22small%22%7D`;
return compositeQuery;
};
// Transform API response to table data
const sessionRecordings: SessionRecording[] = useMemo(() => {
if (
sessionAttributes?.statusCode === 200 &&
sessionAttributes.payload?.stringAttributeValues
) {
return sessionAttributes.payload.stringAttributeValues.map(
(sessionId: string, index: number) => ({
id: String(index + 1),
sessionId,
userName: 'anonymous',
userAgent: 'Mozilla/5.0 (Unknown)',
startTime: new Date().toISOString(), // You can extract timestamp from sessionId if available
duration: 0,
pageViews: 0,
country: 'Unknown',
city: 'Unknown',
device: 'Unknown',
browser: 'Unknown',
os: 'Unknown',
status: 'completed' as const,
hasErrors: false,
recordingUrl: `/session/${sessionId}?${getQueryParams(sessionId)}`,
}),
);
}
return [];
}, [sessionAttributes]);
// Log the response for debugging
if (sessionAttributes && sessionAttributes.statusCode === 200) {
console.log('Session attributes fetched:', sessionAttributes.payload);
}
if (error) {
console.error('Error fetching session attributes:', error);
}
const handleSessionClick = (record: SessionRecording): void => {
history.push(
`/session-recordings/${record.sessionId}?${getQueryParams(
record.sessionId,
)}`,
);
};
const handlePlayClick = (
e: React.MouseEvent,
record: SessionRecording,
): void => {
e.stopPropagation();
history.push(
`/session-recordings/${record.sessionId}?${getQueryParams(
record.sessionId,
)}`,
);
};
const columns = [
{
title: 'Session Name',
dataIndex: 'sessionId',
key: 'sessionId',
width: 200,
render: (sessionId: string) => <Text strong>{sessionId}</Text>,
},
{
title: 'Actions',
key: 'actions',
width: 100,
render: (_: any, record: SessionRecording) => (
<Button
type="primary"
size="small"
icon={<PlayCircle size={14} />}
onClick={(e) => handlePlayClick(e, record)}
className="play-button"
>
Play
</Button>
),
},
];
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="session-recordings-page">
<div className="page-header">
<div className="header-content">
<Title level={2} className="page-title">
Session Recordings
</Title>
<Text type="secondary" className="page-description">
Click play to view session recordings
</Text>
</div>
</div>
<div className="page-content">
<Card className="table-card">
<ResizeTable
columns={columns}
dataSource={sessionRecordings}
rowKey="id"
loading={isLoading}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} of ${total} recordings`,
}}
className="session-recordings-table"
onRow={(record) => ({
onClick: () => handleSessionClick(record),
className: 'clickable-row',
})}
/>
</Card>
</div>
</div>
</Sentry.ErrorBoundary>
);
}

View File

@@ -0,0 +1,221 @@
.session-recordings-page {
height: 100%;
background: var(--bg-ink-400);
padding: 0;
.page-header {
background: var(--bg-ink-500);
border-bottom: 1px solid var(--bg-slate-400);
padding: 24px 24px 16px;
margin-bottom: 0;
.header-content {
.page-title {
margin: 0 0 8px 0;
color: var(--bg-vanilla-100);
font-weight: 600;
font-size: 24px;
line-height: 32px;
}
.page-description {
font-size: 14px;
line-height: 20px;
color: var(--bg-vanilla-400);
margin: 0;
}
}
}
.page-content {
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
.table-card {
background: var(--bg-ink-500);
border: 1px solid var(--bg-slate-400);
border-radius: 6px;
flex: 1;
.ant-card-body {
padding: 16px;
}
.session-recordings-table {
.ant-table {
background: transparent;
.ant-table-thead > tr > th {
background: var(--bg-ink-400);
border-bottom: 1px solid var(--bg-slate-400);
color: var(--bg-vanilla-300);
font-weight: 500;
font-size: 12px;
line-height: 16px;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 12px 16px;
}
.ant-table-tbody > tr > td {
background: transparent;
border-bottom: 1px solid var(--bg-slate-400);
padding: 16px;
color: var(--bg-vanilla-100);
}
.ant-table-tbody > tr:hover > td {
background: var(--bg-ink-400);
}
.clickable-row {
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
background: var(--bg-ink-400);
}
}
}
.ant-pagination {
margin: 16px 0 0 0;
text-align: right;
.ant-pagination-item,
.ant-pagination-prev,
.ant-pagination-next {
background: var(--bg-ink-400);
border: 1px solid var(--bg-slate-400);
color: var(--bg-vanilla-400);
&:hover {
border-color: var(--bg-sakura-500);
color: var(--bg-vanilla-100);
}
&.ant-pagination-item-active {
background: var(--bg-sakura-500);
border-color: var(--bg-sakura-500);
color: var(--bg-ink-500);
}
}
.ant-pagination-options {
.ant-select {
.ant-select-selector {
background: var(--bg-ink-400);
border: 1px solid var(--bg-slate-400);
color: var(--bg-vanilla-100);
}
}
}
}
}
}
}
}
// Play button styles
.play-button {
background: var(--bg-sakura-500);
border-color: var(--bg-sakura-500);
color: var(--bg-ink-500);
&:hover {
background: var(--bg-sakura-600);
border-color: var(--bg-sakura-600);
}
.anticon {
margin-right: 4px;
}
}
// Light mode overrides
.lightMode {
.session-recordings-page {
background: var(--bg-vanilla-100);
.page-header {
background: var(--bg-vanilla-200);
border-bottom: 1px solid var(--bg-slate-300);
.header-content {
.page-title {
color: var(--bg-ink-500);
}
.page-description {
color: var(--bg-ink-400);
}
}
}
.page-content {
.table-card {
background: var(--bg-vanilla-200);
border: 1px solid var(--bg-slate-300);
.session-recordings-table {
.ant-table {
.ant-table-thead > tr > th {
background: var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-slate-300);
color: var(--bg-ink-400);
}
.ant-table-tbody > tr > td {
border-bottom: 1px solid var(--bg-slate-300);
color: var(--bg-ink-500);
}
.ant-table-tbody > tr:hover > td {
background: var(--bg-vanilla-300);
}
.clickable-row:hover {
background: var(--bg-vanilla-300);
}
}
.ant-pagination {
.ant-pagination-item,
.ant-pagination-prev,
.ant-pagination-next {
background: var(--bg-vanilla-300);
border: 1px solid var(--bg-slate-300);
color: var(--bg-ink-400);
&:hover {
border-color: var(--bg-sakura-500);
color: var(--bg-ink-500);
}
}
.ant-pagination-options {
.ant-select {
.ant-select-selector {
background: var(--bg-vanilla-300);
border: 1px solid var(--bg-slate-300);
color: var(--bg-ink-500);
}
}
}
}
}
}
}
}
}
// Responsive design
@media (max-width: 768px) {
.session-recordings-page {
.page-content {
padding: 16px;
}
}
}

View File

@@ -0,0 +1,17 @@
export interface SessionRecording {
id: string;
sessionId: string;
userName: string;
userAgent: string;
startTime: string;
duration: number; // seconds
pageViews: number;
country: string;
city: string;
device: string;
browser: string;
os: string;
status: 'completed' | 'in_progress' | 'failed';
hasErrors: boolean;
recordingUrl: string;
}

View File

@@ -17,9 +17,7 @@ export interface PayloadPropsTraces {
}
export interface PayloadPropsLogs {
version: 'v1' | 'v2';
default_ttl_days: number;
logs_ttl_duration_hrs?: number;
logs_ttl_duration_hrs: number;
logs_move_ttl_duration_hrs?: number;
status: TStatus;
expected_logs_ttl_duration_hrs?: number;

View File

@@ -7,23 +7,6 @@ export interface Props {
toColdDuration?: string;
}
export interface PropsV2 {
type: TTTLType;
defaultTTLDays: number;
coldStorageVolume: string;
coldStorageDuration: number;
ttlConditions: {
conditions: {
key: string;
values: string[];
}[];
ttlDays: number;
}[];
}
export interface PayloadProps {
success: 'message';
}
export interface PayloadPropsV2 {
message: string;
}

View File

@@ -108,6 +108,12 @@ const config = {
},
{
test: /\.css$/,
include: /node_modules/,
use: [styleLoader, cssLoader],
},
{
test: /\.css$/,
exclude: /node_modules/,
use: [
styleLoader,
{

View File

@@ -3834,24 +3834,6 @@
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-visually-hidden" "1.0.3"
"@radix-ui/react-tooltip@^1.2.6":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz#3f50267e25bccfc9e20bb3036bfd9ab4c2c30c2c"
integrity sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==
dependencies:
"@radix-ui/primitive" "1.1.3"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-dismissable-layer" "1.1.11"
"@radix-ui/react-id" "1.1.1"
"@radix-ui/react-popper" "1.2.8"
"@radix-ui/react-portal" "1.1.9"
"@radix-ui/react-presence" "1.1.5"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-slot" "1.2.3"
"@radix-ui/react-use-controllable-state" "1.2.2"
"@radix-ui/react-visually-hidden" "1.2.3"
"@radix-ui/react-use-callback-ref@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a"
@@ -3952,13 +3934,6 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-visually-hidden@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz#a8c38c8607735dc9f05c32f87ab0f9c2b109efbf"
integrity sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==
dependencies:
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/rect@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.1.tgz#bf8e7d947671996da2e30f4904ece343bc4a883f"
@@ -4071,6 +4046,11 @@
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.20.0.tgz#03554155b45d8b529adf635b2f6ad1165d70d8b4"
integrity sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==
"@rrweb/types@2.0.0-alpha.18", "@rrweb/types@^2.0.0-alpha.4":
version "2.0.0-alpha.18"
resolved "https://registry.yarnpkg.com/@rrweb/types/-/types-2.0.0-alpha.18.tgz#e1d9af844cebbf30a2be8808f6cf64f5df3e7f50"
integrity sha512-iMH3amHthJZ9x3gGmBPmdfim7wLGygC2GciIkw2A6SO8giSn8PHYtRT8OKNH4V+k3SZ6RSnYHcTQxBA7pSWZ3Q==
"@sentry-internal/browser-utils@8.41.0":
version "8.41.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.41.0.tgz#9dc30a8c88aa6e1e542e5acae29ceabd1b377cc4"
@@ -4309,53 +4289,6 @@
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/sonner@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@signozhq/sonner/-/sonner-0.1.0.tgz#1310cc530c60459608246550eb977a1ae27b6ce4"
integrity sha512-P4gc1WdNiX89FZIAhIvR4Bj3sdL7VIpoM80L6otnoeLCZhyFfEOXHGAElvO2z7BuBqJBp1f3pdVDrBQNE6bhyw==
dependencies:
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-slot" "^1.1.0"
class-variance-authority "^0.7.0"
clsx "^2.1.1"
lucide-react "^0.445.0"
next-themes "^0.4.6"
sonner "^2.0.7"
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/table@0.3.7":
version "0.3.7"
resolved "https://registry.yarnpkg.com/@signozhq/table/-/table-0.3.7.tgz#895b710c02af124dfb5117e02bbc6d80ce062063"
integrity sha512-XDwRHBTf2q48MOuxkzzr0htWd0/mmvgHoZLl0WAMLk2gddbbNHg9hkCPfVARYOke6mX8Z/4T3e7dzgkRUhDGDg==
dependencies:
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-slot" "^1.1.0"
"@signozhq/tooltip" "0.0.2"
"@tanstack/react-table" "^8.21.3"
"@tanstack/react-virtual" "^3.13.9"
"@types/lodash-es" "^4.17.12"
class-variance-authority "^0.7.0"
clsx "^2.1.1"
lodash-es "^4.17.21"
lucide-react "^0.445.0"
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/tooltip@0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@signozhq/tooltip/-/tooltip-0.0.2.tgz#bb4e2681868fa2e06db78eff5872ffb2a78b93b6"
integrity sha512-itxvrFq0SHr+xTabd7Tf/O9Gfq45kpIyQ5qklDRfhbxr4PIVSBw5XMr2OgzOPbW7P8S9fugCwMu6gPyAZ0SM5A==
dependencies:
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-slot" "^1.1.0"
"@radix-ui/react-tooltip" "^1.2.6"
class-variance-authority "^0.7.0"
clsx "^2.1.1"
lucide-react "^0.445.0"
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@sinclair/typebox@^0.25.16":
version "0.25.24"
resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz"
@@ -4399,13 +4332,6 @@
dependencies:
"@tanstack/table-core" "8.20.5"
"@tanstack/react-table@^8.21.3":
version "8.21.3"
resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.21.3.tgz#2c38c747a5731c1a07174fda764b9c2b1fb5e91b"
integrity sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==
dependencies:
"@tanstack/table-core" "8.21.3"
"@tanstack/react-virtual@3.11.2":
version "3.11.2"
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz#d6b9bd999c181f0a2edce270c87a2febead04322"
@@ -4413,33 +4339,16 @@
dependencies:
"@tanstack/virtual-core" "3.11.2"
"@tanstack/react-virtual@^3.13.9":
version "3.13.12"
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz#d372dc2783739cc04ec1a728ca8203937687a819"
integrity sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==
dependencies:
"@tanstack/virtual-core" "3.13.12"
"@tanstack/table-core@8.20.5":
version "8.20.5"
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d"
integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==
"@tanstack/table-core@8.21.3":
version "8.21.3"
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c"
integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==
"@tanstack/virtual-core@3.11.2":
version "3.11.2"
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212"
integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==
"@tanstack/virtual-core@3.13.12":
version "3.13.12"
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz#1dff176df9cc8f93c78c5e46bcea11079b397578"
integrity sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==
"@testing-library/dom@^8.5.0":
version "8.20.0"
resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz"
@@ -4527,6 +4436,11 @@
resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz"
integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
"@tsconfig/svelte@^1.0.0":
version "1.0.13"
resolved "https://registry.yarnpkg.com/@tsconfig/svelte/-/svelte-1.0.13.tgz#2fa34376627192c0d643ce54964915e2bd3a58e4"
integrity sha512-5lYJP45Xllo4yE/RUBccBT32eBlRDbqN8r1/MIvQbKxW3aFqaYPCNgm8D5V20X4ShHcwvYWNlKg3liDh1MlBoA==
"@tweenjs/tween.js@18 - 19", "@tweenjs/tween.js@19":
version "19.0.0"
resolved "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-19.0.0.tgz"
@@ -4657,6 +4571,11 @@
tapable "^2.0.0"
webpack "^5.1.0"
"@types/css-font-loading-module@0.0.7":
version "0.0.7"
resolved "https://registry.yarnpkg.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz#2f98ede46acc0975de85c0b7b0ebe06041d24601"
integrity sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==
"@types/d3-array@3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.3.tgz#87d990bf504d14ad6b16766979d04e943c046dac"
@@ -4921,13 +4840,6 @@
dependencies:
"@types/node" "*"
"@types/lodash-es@^4.17.12":
version "4.17.12"
resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b"
integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==
dependencies:
"@types/lodash" "*"
"@types/lodash-es@^4.17.4":
version "4.17.7"
resolved "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.7.tgz"
@@ -5827,6 +5739,11 @@
resolved "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz"
integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==
"@xstate/fsm@^1.4.0":
version "1.6.5"
resolved "https://registry.yarnpkg.com/@xstate/fsm/-/fsm-1.6.5.tgz#f599e301997ad7e3c572a0b1ff0696898081bea5"
integrity sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==
"@xstate/react@^3.0.0":
version "3.2.2"
resolved "https://registry.npmjs.org/@xstate/react/-/react-3.2.2.tgz"
@@ -6801,6 +6718,11 @@ balanced-match@^1.0.0:
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base64-arraybuffer@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
@@ -9500,7 +9422,7 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
fflate@^0.4.8:
fflate@^0.4.4, fflate@^0.4.8:
version "0.4.8"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
@@ -13258,6 +13180,11 @@ minipass@^4.2.4:
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c"
integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==
mitt@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
mkdirp@^0.5.6:
version "0.5.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
@@ -13429,11 +13356,6 @@ new-array@^1.0.0:
resolved "https://registry.npmjs.org/new-array/-/new-array-1.0.0.tgz"
integrity sha512-K5AyFYbuHZ4e/ti52y7k18q8UHsS78FlRd85w2Fmsd6AkuLipDihPflKC0p3PN5i8II7+uHxo+CtkLiJDfmS5A==
next-themes@^0.4.6:
version "0.4.6"
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.6.tgz#8d7e92d03b8fea6582892a50a928c9b23502e8b6"
integrity sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==
ngraph.events@^1.0.0, ngraph.events@^1.2.1:
version "1.2.2"
resolved "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.2.tgz"
@@ -16097,6 +16019,40 @@ robust-predicates@^3.0.2:
resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
rrdom@^0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/rrdom/-/rrdom-0.1.7.tgz#f2f49bfd01b59291bb7b0d981371a5e02a18e2aa"
integrity sha512-ZLd8f14z9pUy2Hk9y636cNv5Y2BMnNEY99wxzW9tD2BLDfe1xFxtLjB4q/xCBYo6HRe0wofzKzjm4JojmpBfFw==
dependencies:
rrweb-snapshot "^2.0.0-alpha.4"
rrweb-player@1.0.0-alpha.4:
version "1.0.0-alpha.4"
resolved "https://registry.yarnpkg.com/rrweb-player/-/rrweb-player-1.0.0-alpha.4.tgz#57576343aaff6c6fb266689fd5d63092be46967c"
integrity sha512-Wlmn9GZ5Fdqa37vd3TzsYdLl/JWEvXNUrLCrYpnOwEgmY409HwVIvvA5aIo7k582LoKgdRCsB87N+f0oWAR0Kg==
dependencies:
"@tsconfig/svelte" "^1.0.0"
rrweb "^2.0.0-alpha.4"
rrweb-snapshot@^2.0.0-alpha.4:
version "2.0.0-alpha.4"
resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.4.tgz#2801bf5946177b9d685a01661a62d9d2e958f174"
integrity sha512-KQ2OtPpXO5jLYqg1OnXS/Hf+EzqnZyP5A+XPqBCjYpj3XIje/Od4gdUwjbFo3cVuWq5Cw5Y1d3/xwgIS7/XpQQ==
rrweb@^2.0.0-alpha.4:
version "2.0.0-alpha.4"
resolved "https://registry.yarnpkg.com/rrweb/-/rrweb-2.0.0-alpha.4.tgz#3c7cf2f1bcf44f7a88dd3fad00ee8d6dd711f258"
integrity sha512-wEHUILbxDPcNwkM3m4qgPgXAiBJyqCbbOHyVoNEVBJzHszWEFYyTbrZqUdeb1EfmTRC2PsumCIkVcomJ/xcOzA==
dependencies:
"@rrweb/types" "^2.0.0-alpha.4"
"@types/css-font-loading-module" "0.0.7"
"@xstate/fsm" "^1.4.0"
base64-arraybuffer "^1.0.1"
fflate "^0.4.4"
mitt "^3.0.0"
rrdom "^0.1.7"
rrweb-snapshot "^2.0.0-alpha.4"
rtl-css-js@^1.14.0, rtl-css-js@^1.16.1:
version "1.16.1"
resolved "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz"
@@ -16538,11 +16494,6 @@ sockjs@^0.3.24:
uuid "^8.3.2"
websocket-driver "^0.7.4"
sonner@^2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/sonner/-/sonner-2.0.7.tgz#810c1487a67ec3370126e0f400dfb9edddc3e4f6"
integrity sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==
sort-asc@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz"

View File

@@ -1,503 +0,0 @@
package thirdpartyapi
import (
"fmt"
"github.com/SigNoz/signoz/pkg/types/thirdpartyapitypes"
"net"
"regexp"
"time"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const (
urlPathKeyLegacy = "http.url"
serverAddressKeyLegacy = "net.peer.name"
urlPathKey = "url.full"
serverAddressKey = "server.address"
)
var defaultStepInterval = 60 * time.Second
type SemconvFieldMapping struct {
LegacyField string
CurrentField string
FieldType telemetrytypes.FieldDataType
Context telemetrytypes.FieldContext
}
var dualSemconvGroupByKeys = map[string][]qbtypes.GroupByKey{
"server": {
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: serverAddressKey,
FieldDataType: telemetrytypes.FieldDataTypeString,
FieldContext: telemetrytypes.FieldContextAttribute,
Signal: telemetrytypes.SignalTraces,
},
},
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: serverAddressKeyLegacy,
FieldDataType: telemetrytypes.FieldDataTypeString,
FieldContext: telemetrytypes.FieldContextAttribute,
Signal: telemetrytypes.SignalTraces,
},
},
},
"url": {
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: urlPathKey,
FieldDataType: telemetrytypes.FieldDataTypeString,
FieldContext: telemetrytypes.FieldContextAttribute,
Signal: telemetrytypes.SignalTraces,
},
},
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: urlPathKeyLegacy,
FieldDataType: telemetrytypes.FieldDataTypeString,
FieldContext: telemetrytypes.FieldContextAttribute,
Signal: telemetrytypes.SignalTraces,
},
},
},
}
func MergeSemconvColumns(result *qbtypes.QueryRangeResponse) *qbtypes.QueryRangeResponse {
if result == nil || result.Data.Results == nil {
return result
}
for _, res := range result.Data.Results {
scalarData, ok := res.(*qbtypes.ScalarData)
if !ok {
continue
}
serverAddressKeyIdx := -1
serverAddressKeyLegacyIdx := -1
for i, col := range scalarData.Columns {
if col.Name == serverAddressKey {
serverAddressKeyIdx = i
} else if col.Name == serverAddressKeyLegacy {
serverAddressKeyLegacyIdx = i
}
}
if serverAddressKeyIdx == -1 || serverAddressKeyLegacyIdx == -1 {
continue
}
var newRows [][]any
for _, row := range scalarData.Data {
if len(row) <= serverAddressKeyIdx || len(row) <= serverAddressKeyLegacyIdx {
continue
}
var serverName any
if isValidValue(row[serverAddressKeyIdx]) {
serverName = row[serverAddressKeyIdx]
} else if isValidValue(row[serverAddressKeyLegacyIdx]) {
serverName = row[serverAddressKeyLegacyIdx]
}
if serverName != nil {
newRow := make([]any, len(row)-1)
newRow[0] = serverName
targetIdx := 1
for i, val := range row {
if i != serverAddressKeyLegacyIdx && i != serverAddressKeyIdx {
if targetIdx < len(newRow) {
newRow[targetIdx] = val
targetIdx++
}
}
}
newRows = append(newRows, newRow)
}
}
newColumns := make([]*qbtypes.ColumnDescriptor, len(scalarData.Columns)-1)
targetIdx := 0
for i, col := range scalarData.Columns {
if i == serverAddressKeyIdx {
newCol := &qbtypes.ColumnDescriptor{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: serverAddressKeyLegacy,
FieldDataType: col.FieldDataType,
FieldContext: col.FieldContext,
Signal: col.Signal,
},
QueryName: col.QueryName,
AggregationIndex: col.AggregationIndex,
Meta: col.Meta,
Type: col.Type,
}
newColumns[targetIdx] = newCol
targetIdx++
} else if i != serverAddressKeyLegacyIdx {
newColumns[targetIdx] = col
targetIdx++
}
}
scalarData.Columns = newColumns
scalarData.Data = newRows
}
return result
}
func isValidValue(val any) bool {
if val == nil {
return false
}
if str, ok := val.(string); ok {
return str != "" && str != "n/a"
}
return true
}
func FilterResponse(results []*qbtypes.QueryRangeResponse) []*qbtypes.QueryRangeResponse {
filteredResults := make([]*qbtypes.QueryRangeResponse, 0, len(results))
for _, res := range results {
if res.Data.Results == nil {
continue
}
filteredData := make([]any, 0, len(res.Data.Results))
for _, result := range res.Data.Results {
if result == nil {
filteredData = append(filteredData, result)
continue
}
switch resultData := result.(type) {
case *qbtypes.TimeSeriesData:
if resultData.Aggregations != nil {
for _, agg := range resultData.Aggregations {
filteredSeries := make([]*qbtypes.TimeSeries, 0, len(agg.Series))
for _, series := range agg.Series {
if shouldIncludeSeries(series) {
filteredSeries = append(filteredSeries, series)
}
}
agg.Series = filteredSeries
}
}
case *qbtypes.RawData:
filteredRows := make([]*qbtypes.RawRow, 0, len(resultData.Rows))
for _, row := range resultData.Rows {
if shouldIncludeRow(row) {
filteredRows = append(filteredRows, row)
}
}
resultData.Rows = filteredRows
}
filteredData = append(filteredData, result)
}
res.Data.Results = filteredData
filteredResults = append(filteredResults, res)
}
return filteredResults
}
func shouldIncludeSeries(series *qbtypes.TimeSeries) bool {
for _, label := range series.Labels {
if label.Key.Name == serverAddressKeyLegacy || label.Key.Name == serverAddressKey {
if strVal, ok := label.Value.(string); ok {
if net.ParseIP(strVal) != nil {
return false
}
}
}
}
return true
}
func shouldIncludeRow(row *qbtypes.RawRow) bool {
if row.Data != nil {
for _, key := range []string{serverAddressKeyLegacy, serverAddressKey} {
if domainVal, ok := row.Data[key]; ok {
if domainStr, ok := domainVal.(string); ok {
if net.ParseIP(domainStr) != nil {
return false
}
}
}
}
}
return true
}
func containsKindStringOverride(expression string) bool {
kindStringPattern := regexp.MustCompile(`kind_string\s*[!=<>]+`)
return kindStringPattern.MatchString(expression)
}
func mergeGroupBy(base, additional []qbtypes.GroupByKey) []qbtypes.GroupByKey {
return append(base, additional...)
}
func BuildDomainList(req *thirdpartyapitypes.ThirdPartyApiRequest) (*qbtypes.QueryRangeRequest, error) {
if err := req.Validate(); err != nil {
return nil, err
}
queries := []qbtypes.QueryEnvelope{
buildEndpointsQuery(req),
buildLastSeenQuery(req),
buildRpsQuery(req),
buildErrorQuery(req),
buildTotalSpanQuery(req),
buildP99Query(req),
buildErrorRateFormula(),
}
return &qbtypes.QueryRangeRequest{
SchemaVersion: "v5",
Start: req.Start,
End: req.End,
RequestType: qbtypes.RequestTypeScalar,
CompositeQuery: qbtypes.CompositeQuery{
Queries: queries,
},
FormatOptions: &qbtypes.FormatOptions{
FormatTableResultForUI: true,
},
}, nil
}
func BuildDomainInfo(req *thirdpartyapitypes.ThirdPartyApiRequest) (*qbtypes.QueryRangeRequest, error) {
if err := req.Validate(); err != nil {
return nil, err
}
queries := []qbtypes.QueryEnvelope{
buildEndpointsInfoQuery(req),
buildP99InfoQuery(req),
buildErrorRateInfoQuery(req),
buildLastSeenInfoQuery(req),
}
return &qbtypes.QueryRangeRequest{
SchemaVersion: "v5",
Start: req.Start,
End: req.End,
RequestType: qbtypes.RequestTypeScalar,
CompositeQuery: qbtypes.CompositeQuery{
Queries: queries,
},
FormatOptions: &qbtypes.FormatOptions{
FormatTableResultForUI: true,
},
}, nil
}
func buildEndpointsQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
return qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Name: "endpoints",
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
Aggregations: []qbtypes.TraceAggregation{
{Expression: "count_distinct(http.url)"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
},
}
}
func buildLastSeenQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
return qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Name: "lastseen",
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
Aggregations: []qbtypes.TraceAggregation{
{Expression: "max(timestamp)"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
},
}
}
func buildRpsQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
return qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Name: "rps",
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
Aggregations: []qbtypes.TraceAggregation{
{Expression: "rate()"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
},
}
}
func buildErrorQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
return qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Name: "error",
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
Aggregations: []qbtypes.TraceAggregation{
{Expression: "count()"},
},
Filter: buildErrorFilter(req.Filter),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
},
}
}
func buildTotalSpanQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
return qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Name: "total_span",
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
Aggregations: []qbtypes.TraceAggregation{
{Expression: "count()"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
},
}
}
func buildP99Query(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
return qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Name: "p99",
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
Aggregations: []qbtypes.TraceAggregation{
{Expression: "p99(duration_nano)"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["server"], req.GroupBy),
},
}
}
func buildErrorRateFormula() qbtypes.QueryEnvelope {
return qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeFormula,
Spec: qbtypes.QueryBuilderFormula{
Name: "error_rate",
Expression: "(error/total_span)*100",
},
}
}
func buildEndpointsInfoQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
return qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Name: "endpoints",
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
Aggregations: []qbtypes.TraceAggregation{
{Expression: "rate(http.url)"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: mergeGroupBy(dualSemconvGroupByKeys["url"], req.GroupBy),
},
}
}
func buildP99InfoQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
return qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Name: "p99",
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
Aggregations: []qbtypes.TraceAggregation{
{Expression: "p99(duration_nano)"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: req.GroupBy,
},
}
}
func buildErrorRateInfoQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
return qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Name: "error_rate",
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
Aggregations: []qbtypes.TraceAggregation{
{Expression: "rate()"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: req.GroupBy,
},
}
}
func buildLastSeenInfoQuery(req *thirdpartyapitypes.ThirdPartyApiRequest) qbtypes.QueryEnvelope {
return qbtypes.QueryEnvelope{
Type: qbtypes.QueryTypeBuilder,
Spec: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Name: "lastseen",
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: defaultStepInterval},
Aggregations: []qbtypes.TraceAggregation{
{Expression: "max(timestamp)"},
},
Filter: buildBaseFilter(req.Filter),
GroupBy: req.GroupBy,
},
}
}
func buildBaseFilter(additionalFilter *qbtypes.Filter) *qbtypes.Filter {
baseExpression := fmt.Sprintf("(%s EXISTS OR %s EXISTS) AND kind_string = 'Client'",
urlPathKeyLegacy, urlPathKey)
if additionalFilter != nil && additionalFilter.Expression != "" {
if containsKindStringOverride(additionalFilter.Expression) {
return &qbtypes.Filter{Expression: baseExpression}
}
baseExpression = fmt.Sprintf("(%s) AND (%s)", baseExpression, additionalFilter.Expression)
}
return &qbtypes.Filter{Expression: baseExpression}
}
func buildErrorFilter(additionalFilter *qbtypes.Filter) *qbtypes.Filter {
errorExpression := fmt.Sprintf("has_error = true AND (%s EXISTS OR %s EXISTS) AND kind_string = 'Client'",
urlPathKeyLegacy, urlPathKey)
if additionalFilter != nil && additionalFilter.Expression != "" {
if containsKindStringOverride(additionalFilter.Expression) {
return &qbtypes.Filter{Expression: errorExpression}
}
errorExpression = fmt.Sprintf("(%s) AND (%s)", errorExpression, additionalFilter.Expression)
}
return &qbtypes.Filter{Expression: errorExpression}
}

View File

@@ -1,233 +0,0 @@
package thirdpartyapi
import (
"github.com/SigNoz/signoz/pkg/types/thirdpartyapitypes"
"testing"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
)
func TestFilterResponse(t *testing.T) {
tests := []struct {
name string
input []*qbtypes.QueryRangeResponse
expected []*qbtypes.QueryRangeResponse
}{
{
name: "should filter out IP addresses from series labels",
input: []*qbtypes.QueryRangeResponse{
{
Data: qbtypes.QueryData{
Results: []any{
&qbtypes.TimeSeriesData{
Aggregations: []*qbtypes.AggregationBucket{
{
Series: []*qbtypes.TimeSeries{
{
Labels: []*qbtypes.Label{
{
Key: telemetrytypes.TelemetryFieldKey{Name: "net.peer.name"},
Value: "192.168.1.1",
},
},
},
{
Labels: []*qbtypes.Label{
{
Key: telemetrytypes.TelemetryFieldKey{Name: "net.peer.name"},
Value: "example.com",
},
},
},
},
},
},
},
},
},
},
},
expected: []*qbtypes.QueryRangeResponse{
{
Data: qbtypes.QueryData{
Results: []any{
&qbtypes.TimeSeriesData{
Aggregations: []*qbtypes.AggregationBucket{
{
Series: []*qbtypes.TimeSeries{
{
Labels: []*qbtypes.Label{
{
Key: telemetrytypes.TelemetryFieldKey{Name: "net.peer.name"},
Value: "example.com",
},
},
},
},
},
},
},
},
},
},
},
},
{
name: "should filter out IP addresses from raw data",
input: []*qbtypes.QueryRangeResponse{
{
Data: qbtypes.QueryData{
Results: []any{
&qbtypes.RawData{
Rows: []*qbtypes.RawRow{
{
Data: map[string]any{
"net.peer.name": "192.168.1.1",
},
},
{
Data: map[string]any{
"net.peer.name": "example.com",
},
},
},
},
},
},
},
},
expected: []*qbtypes.QueryRangeResponse{
{
Data: qbtypes.QueryData{
Results: []any{
&qbtypes.RawData{
Rows: []*qbtypes.RawRow{
{
Data: map[string]any{
"net.peer.name": "example.com",
},
},
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := FilterResponse(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestBuildDomainList(t *testing.T) {
tests := []struct {
name string
input *thirdpartyapitypes.ThirdPartyApiRequest
wantErr bool
}{
{
name: "basic domain list query",
input: &thirdpartyapitypes.ThirdPartyApiRequest{
Start: 1000,
End: 2000,
},
wantErr: false,
},
{
name: "with filters and group by",
input: &thirdpartyapitypes.ThirdPartyApiRequest{
Start: 1000,
End: 2000,
Filter: &qbtypes.Filter{
Expression: "test = 'value'",
},
GroupBy: []qbtypes.GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "test",
},
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := BuildDomainList(tt.input)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, tt.input.Start, result.Start)
assert.Equal(t, tt.input.End, result.End)
assert.NotNil(t, result.CompositeQuery)
assert.Len(t, result.CompositeQuery.Queries, 7) // endpoints, lastseen, rps, error, total_span, p99, error_rate
assert.Equal(t, "v5", result.SchemaVersion)
assert.Equal(t, qbtypes.RequestTypeScalar, result.RequestType)
})
}
}
func TestBuildDomainInfo(t *testing.T) {
tests := []struct {
name string
input *thirdpartyapitypes.ThirdPartyApiRequest
wantErr bool
}{
{
name: "basic domain info query",
input: &thirdpartyapitypes.ThirdPartyApiRequest{
Start: 1000,
End: 2000,
},
wantErr: false,
},
{
name: "with filters and group by",
input: &thirdpartyapitypes.ThirdPartyApiRequest{
Start: 1000,
End: 2000,
Filter: &qbtypes.Filter{
Expression: "test = 'value'",
},
GroupBy: []qbtypes.GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "test",
},
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := BuildDomainInfo(tt.input)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, tt.input.Start, result.Start)
assert.Equal(t, tt.input.End, result.End)
assert.NotNil(t, result.CompositeQuery)
assert.Len(t, result.CompositeQuery.Queries, 4) // endpoints, p99, error_rate, lastseen
assert.Equal(t, "v5", result.SchemaVersion)
assert.Equal(t, qbtypes.RequestTypeScalar, result.RequestType)
})
}
}

View File

@@ -21,8 +21,6 @@ import (
"github.com/SigNoz/signoz/pkg/types/emailtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/google/uuid"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
type Module struct {
@@ -230,11 +228,11 @@ func (m *Module) UpdateUser(ctx context.Context, orgID string, id string, user *
// if the role is updated then send an email
if existingUser.Role != updatedUser.Role {
if err := m.emailing.SendHTML(ctx, existingUser.Email, "Your Role Has Been Updated in SigNoz", emailtypes.TemplateNameUpdateRole, map[string]any{
if err := m.emailing.SendHTML(ctx, existingUser.Email, "Your Role is updated in SigNoz", emailtypes.TemplateNameUpdateRole, map[string]any{
"CustomerName": existingUser.DisplayName,
"UpdatedByEmail": requestor.Email,
"OldRole": cases.Title(language.English).String(strings.ToLower(existingUser.Role)),
"NewRole": cases.Title(language.English).String(strings.ToLower(updatedUser.Role)),
"OldRole": existingUser.Role,
"NewRole": updatedUser.Role,
}); err != nil {
m.settings.Logger().ErrorContext(ctx, "failed to send email", "error", err)
}

View File

@@ -3,14 +3,10 @@ package app
import (
"bytes"
"context"
"database/sql"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/SigNoz/signoz/pkg/modules/thirdpartyapi"
//qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"io"
"math"
"net/http"
@@ -48,6 +44,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/inframetrics"
queues2 "github.com/SigNoz/signoz/pkg/query-service/app/integrations/messagingQueues/queues"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations/thirdPartyApi"
"github.com/SigNoz/signoz/pkg/query-service/app/logs"
logsv3 "github.com/SigNoz/signoz/pkg/query-service/app/logs/v3"
logsv4 "github.com/SigNoz/signoz/pkg/query-service/app/logs/v4"
@@ -671,10 +668,6 @@ func (aH *APIHandler) getRule(w http.ResponseWriter, r *http.Request) {
ruleResponse, err := aH.ruleManager.GetRule(r.Context(), id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("rule not found")}, nil)
return
}
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
@@ -1389,10 +1382,6 @@ func (aH *APIHandler) deleteRule(w http.ResponseWriter, r *http.Request) {
err := aH.ruleManager.DeleteRule(r.Context(), id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("rule not found")}, nil)
return
}
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
@@ -1421,10 +1410,6 @@ func (aH *APIHandler) patchRule(w http.ResponseWriter, r *http.Request) {
gettableRule, err := aH.ruleManager.PatchRule(r.Context(), string(body), id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("rule not found")}, nil)
return
}
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
@@ -1451,10 +1436,6 @@ func (aH *APIHandler) editRule(w http.ResponseWriter, r *http.Request) {
err = aH.ruleManager.EditRule(r.Context(), string(body), id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("rule not found")}, nil)
return
}
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
return
}
@@ -4984,130 +4965,105 @@ func (aH *APIHandler) getQueueOverview(w http.ResponseWriter, r *http.Request) {
}
func (aH *APIHandler) getDomainList(w http.ResponseWriter, r *http.Request) {
// Extract claims from context for organization ID
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(w, err)
return
}
// Parse the request body to get third-party query parameters
thirdPartyQueryRequest, apiErr := ParseRequestBody(r)
if apiErr != nil {
zap.L().Error("Failed to parse request body", zap.Error(apiErr))
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, apiErr.Error()))
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
// Build the v5 query range request for domain listing
queryRangeRequest, err := thirdpartyapi.BuildDomainList(thirdPartyQueryRequest)
queryRangeParams, err := thirdPartyApi.BuildDomainList(thirdPartyQueryRequest)
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
var result []*v3.Result
var errQuriesByName map[string]error
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), orgID, queryRangeParams)
if err != nil {
zap.L().Error("Failed to build domain list query", zap.Error(err))
apiErrObj := errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error())
render.Error(w, apiErrObj)
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
RespondError(w, apiErrObj, errQuriesByName)
return
}
// Validate the v5 query range request
if err := queryRangeRequest.Validate(); err != nil {
zap.L().Error("Query validation failed", zap.Error(err))
apiErrObj := errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error())
render.Error(w, apiErrObj)
return
}
// Execute the query using the v5 querier
result, err := aH.Signoz.Querier.QueryRange(r.Context(), orgID, queryRangeRequest)
result, err = postprocess.PostProcessResult(result, queryRangeParams)
if err != nil {
zap.L().Error("Query execution failed", zap.Error(err))
apiErrObj := errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error())
render.Error(w, apiErrObj)
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
RespondError(w, apiErrObj, errQuriesByName)
return
}
result = thirdpartyapi.MergeSemconvColumns(result)
// Filter IP addresses if ShowIp is false
var finalResult = result
if !thirdPartyQueryRequest.ShowIp {
filteredResults := thirdpartyapi.FilterResponse([]*qbtypes.QueryRangeResponse{result})
if len(filteredResults) > 0 {
finalResult = filteredResults[0]
}
result = thirdPartyApi.FilterResponse(result)
}
// Send the response
aH.Respond(w, finalResult)
resp := v3.QueryRangeResponse{
Result: result,
}
aH.Respond(w, resp)
}
// getDomainInfo handles requests for domain information using v5 query builder
func (aH *APIHandler) getDomainInfo(w http.ResponseWriter, r *http.Request) {
// Extract claims from context for organization ID
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(w, err)
return
}
// Parse the request body to get third-party query parameters
thirdPartyQueryRequest, apiErr := ParseRequestBody(r)
if apiErr != nil {
zap.L().Error("Failed to parse request body", zap.Error(apiErr))
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, apiErr.Error()))
zap.L().Error(apiErr.Err.Error())
RespondError(w, apiErr, nil)
return
}
// Build the v5 query range request for domain info
queryRangeRequest, err := thirdpartyapi.BuildDomainInfo(thirdPartyQueryRequest)
queryRangeParams, err := thirdPartyApi.BuildDomainInfo(thirdPartyQueryRequest)
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
zap.L().Error(err.Error())
RespondError(w, apiErr, nil)
return
}
var result []*v3.Result
var errQuriesByName map[string]error
result, errQuriesByName, err = aH.querierV2.QueryRange(r.Context(), orgID, queryRangeParams)
if err != nil {
zap.L().Error("Failed to build domain info query", zap.Error(err))
apiErrObj := errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error())
render.Error(w, apiErrObj)
apiErrObj := &model.ApiError{Typ: model.ErrorBadData, Err: err}
RespondError(w, apiErrObj, errQuriesByName)
return
}
// Validate the v5 query range request
if err := queryRangeRequest.Validate(); err != nil {
zap.L().Error("Query validation failed", zap.Error(err))
apiErrObj := errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error())
render.Error(w, apiErrObj)
return
}
result = postprocess.TransformToTableForBuilderQueries(result, queryRangeParams)
// Execute the query using the v5 querier
result, err := aH.Signoz.Querier.QueryRange(r.Context(), orgID, queryRangeRequest)
if err != nil {
zap.L().Error("Query execution failed", zap.Error(err))
apiErrObj := errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error())
render.Error(w, apiErrObj)
return
}
result = thirdpartyapi.MergeSemconvColumns(result)
// Filter IP addresses if ShowIp is false
var finalResult *qbtypes.QueryRangeResponse = result
if !thirdPartyQueryRequest.ShowIp {
filteredResults := thirdpartyapi.FilterResponse([]*qbtypes.QueryRangeResponse{result})
if len(filteredResults) > 0 {
finalResult = filteredResults[0]
}
result = thirdPartyApi.FilterResponse(result)
}
// Send the response
aH.Respond(w, finalResult)
resp := v3.QueryRangeResponse{
Result: result,
}
aH.Respond(w, resp)
}
// RegisterTraceFunnelsRoutes adds trace funnels routes

View File

@@ -0,0 +1,13 @@
package thirdPartyApi
import v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
type ThirdPartyApis struct {
Start int64 `json:"start"`
End int64 `json:"end"`
ShowIp bool `json:"show_ip,omitempty"`
Domain int64 `json:"domain,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
Filters v3.FilterSet `json:"filters,omitempty"`
GroupBy []v3.AttributeKey `json:"groupBy,omitempty"`
}

View File

@@ -0,0 +1,598 @@
package thirdPartyApi
import (
"net"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
)
const (
urlPathKey = "http.url"
serverNameKey = "net.peer.name"
)
var defaultStepInterval int64 = 60
func FilterResponse(results []*v3.Result) []*v3.Result {
filteredResults := make([]*v3.Result, 0, len(results))
for _, res := range results {
if res.Table == nil {
continue
}
filteredRows := make([]*v3.TableRow, 0, len(res.Table.Rows))
for _, row := range res.Table.Rows {
if row.Data != nil {
if domainVal, ok := row.Data[serverNameKey]; ok {
if domainStr, ok := domainVal.(string); ok {
if net.ParseIP(domainStr) != nil {
continue
}
}
}
}
filteredRows = append(filteredRows, row)
}
res.Table.Rows = filteredRows
filteredResults = append(filteredResults, res)
}
return filteredResults
}
func getFilterSet(existingFilters []v3.FilterItem, apiFilters v3.FilterSet) []v3.FilterItem {
if len(apiFilters.Items) != 0 {
existingFilters = append(existingFilters, apiFilters.Items...)
}
return existingFilters
}
func getGroupBy(existingGroupBy []v3.AttributeKey, apiGroupBy []v3.AttributeKey) []v3.AttributeKey {
if len(apiGroupBy) != 0 {
existingGroupBy = append(existingGroupBy, apiGroupBy...)
}
return existingGroupBy
}
func BuildDomainList(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, error) {
unixMilliStart := thirdPartyApis.Start
unixMilliEnd := thirdPartyApis.End
builderQueries := make(map[string]*v3.BuilderQuery)
builderQueries["endpoints"] = &v3.BuilderQuery{
QueryName: "endpoints",
Legend: "endpoints",
DataSource: v3.DataSourceTraces,
StepInterval: defaultStepInterval,
AggregateOperator: v3.AggregateOperatorCountDistinct,
AggregateAttribute: v3.AttributeKey{
Key: urlPathKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
TimeAggregation: v3.TimeAggregationCountDistinct,
SpaceAggregation: v3.SpaceAggregationSum,
Filters: &v3.FilterSet{
Operator: "AND",
Items: getFilterSet([]v3.FilterItem{
{
Key: v3.AttributeKey{
Key: urlPathKey,
DataType: v3.AttributeKeyDataTypeString,
IsColumn: false,
Type: v3.AttributeKeyTypeTag,
},
Operator: v3.FilterOperatorExists,
Value: "",
},
{
Key: v3.AttributeKey{
Key: "kind_string",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: true,
},
Operator: "=",
Value: "Client",
},
}, thirdPartyApis.Filters),
},
Expression: "endpoints",
GroupBy: getGroupBy([]v3.AttributeKey{
{
Key: serverNameKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
}, thirdPartyApis.GroupBy),
ReduceTo: v3.ReduceToOperatorAvg,
ShiftBy: 0,
IsAnomaly: false,
}
builderQueries["lastseen"] = &v3.BuilderQuery{
QueryName: "lastseen",
Legend: "lastseen",
DataSource: v3.DataSourceTraces,
StepInterval: defaultStepInterval,
AggregateOperator: v3.AggregateOperatorMax,
AggregateAttribute: v3.AttributeKey{
Key: "timestamp",
},
TimeAggregation: v3.TimeAggregationMax,
SpaceAggregation: v3.SpaceAggregationSum,
Filters: &v3.FilterSet{
Operator: "AND",
Items: getFilterSet([]v3.FilterItem{
{
Key: v3.AttributeKey{
Key: urlPathKey,
DataType: v3.AttributeKeyDataTypeString,
IsColumn: false,
Type: v3.AttributeKeyTypeTag,
},
Operator: v3.FilterOperatorExists,
Value: "",
},
{
Key: v3.AttributeKey{
Key: "kind_string",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: true,
},
Operator: "=",
Value: "Client",
},
}, thirdPartyApis.Filters),
},
Expression: "lastseen",
GroupBy: getGroupBy([]v3.AttributeKey{
{
Key: serverNameKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
}, thirdPartyApis.GroupBy),
ReduceTo: v3.ReduceToOperatorAvg,
ShiftBy: 0,
IsAnomaly: false,
}
builderQueries["rps"] = &v3.BuilderQuery{
QueryName: "rps",
Legend: "rps",
DataSource: v3.DataSourceTraces,
StepInterval: defaultStepInterval,
AggregateOperator: v3.AggregateOperatorRate,
AggregateAttribute: v3.AttributeKey{
Key: "",
},
TimeAggregation: v3.TimeAggregationRate,
SpaceAggregation: v3.SpaceAggregationSum,
Filters: &v3.FilterSet{
Operator: "AND",
Items: getFilterSet([]v3.FilterItem{
{
Key: v3.AttributeKey{
Key: urlPathKey,
DataType: v3.AttributeKeyDataTypeString,
IsColumn: false,
Type: v3.AttributeKeyTypeTag,
},
Operator: v3.FilterOperatorExists,
Value: "",
},
{
Key: v3.AttributeKey{
Key: "kind_string",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: true,
},
Operator: "=",
Value: "Client",
},
}, thirdPartyApis.Filters),
},
Expression: "rps",
GroupBy: getGroupBy([]v3.AttributeKey{
{
Key: serverNameKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
}, thirdPartyApis.GroupBy),
ReduceTo: v3.ReduceToOperatorAvg,
ShiftBy: 0,
IsAnomaly: false,
}
builderQueries["error"] = &v3.BuilderQuery{
QueryName: "error",
DataSource: v3.DataSourceTraces,
StepInterval: defaultStepInterval,
AggregateOperator: v3.AggregateOperatorCount,
AggregateAttribute: v3.AttributeKey{
Key: "span_id",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: true,
},
TimeAggregation: v3.TimeAggregationCount,
SpaceAggregation: v3.SpaceAggregationSum,
Filters: &v3.FilterSet{
Operator: "AND",
Items: getFilterSet([]v3.FilterItem{
{
Key: v3.AttributeKey{
Key: "has_error",
DataType: v3.AttributeKeyDataTypeBool,
IsColumn: true,
},
Operator: "=",
Value: "true",
},
{
Key: v3.AttributeKey{
Key: urlPathKey,
DataType: v3.AttributeKeyDataTypeString,
IsColumn: false,
Type: v3.AttributeKeyTypeTag,
},
Operator: v3.FilterOperatorExists,
Value: "",
},
{
Key: v3.AttributeKey{
Key: "kind_string",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: true,
},
Operator: "=",
Value: "Client",
},
}, thirdPartyApis.Filters),
},
Expression: "error",
GroupBy: getGroupBy([]v3.AttributeKey{
{
Key: serverNameKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
}, thirdPartyApis.GroupBy),
ReduceTo: v3.ReduceToOperatorAvg,
Disabled: true,
ShiftBy: 0,
IsAnomaly: false,
}
builderQueries["total_span"] = &v3.BuilderQuery{
QueryName: "total_span",
DataSource: v3.DataSourceTraces,
StepInterval: defaultStepInterval,
AggregateOperator: v3.AggregateOperatorCount,
AggregateAttribute: v3.AttributeKey{
Key: "span_id",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: true,
},
TimeAggregation: v3.TimeAggregationCount,
SpaceAggregation: v3.SpaceAggregationSum,
Filters: &v3.FilterSet{
Operator: "AND",
Items: getFilterSet([]v3.FilterItem{
{
Key: v3.AttributeKey{
Key: "http.url",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: false,
Type: v3.AttributeKeyTypeTag,
},
Operator: v3.FilterOperatorExists,
Value: "",
},
{
Key: v3.AttributeKey{
Key: "kind_string",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: true,
},
Operator: "=",
Value: "Client",
},
}, thirdPartyApis.Filters),
},
Expression: "total_span",
GroupBy: getGroupBy([]v3.AttributeKey{
{
Key: "net.peer.name",
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
}, thirdPartyApis.GroupBy),
ReduceTo: v3.ReduceToOperatorAvg,
Disabled: true,
ShiftBy: 0,
IsAnomaly: false,
}
builderQueries["p99"] = &v3.BuilderQuery{
QueryName: "p99",
Legend: "p99",
DataSource: v3.DataSourceTraces,
StepInterval: defaultStepInterval,
AggregateOperator: v3.AggregateOperatorP99,
AggregateAttribute: v3.AttributeKey{
Key: "duration_nano",
DataType: v3.AttributeKeyDataTypeFloat64,
IsColumn: true,
},
TimeAggregation: "p99",
SpaceAggregation: v3.SpaceAggregationSum,
Filters: &v3.FilterSet{
Operator: "AND",
Items: getFilterSet([]v3.FilterItem{
{
Key: v3.AttributeKey{
Key: urlPathKey,
DataType: v3.AttributeKeyDataTypeString,
IsColumn: false,
Type: v3.AttributeKeyTypeTag,
},
Operator: v3.FilterOperatorExists,
Value: "",
},
{
Key: v3.AttributeKey{
Key: "kind_string",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: true,
},
Operator: "=",
Value: "Client",
},
}, thirdPartyApis.Filters),
},
Expression: "p99",
GroupBy: getGroupBy([]v3.AttributeKey{
{
Key: serverNameKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
}, thirdPartyApis.GroupBy),
ReduceTo: v3.ReduceToOperatorAvg,
ShiftBy: 0,
IsAnomaly: false,
}
builderQueries["error_rate"] = &v3.BuilderQuery{
QueryName: "error_rate",
Expression: "(error/total_span)*100",
Legend: "error_rate",
Disabled: false,
ShiftBy: 0,
IsAnomaly: false,
}
compositeQuery := &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,
PanelType: v3.PanelTypeTable,
FillGaps: false,
BuilderQueries: builderQueries,
}
queryRangeParams := &v3.QueryRangeParamsV3{
Start: unixMilliStart,
End: unixMilliEnd,
Step: defaultStepInterval,
CompositeQuery: compositeQuery,
Version: "v4",
FormatForWeb: true,
}
return queryRangeParams, nil
}
func BuildDomainInfo(thirdPartyApis *ThirdPartyApis) (*v3.QueryRangeParamsV3, error) {
unixMilliStart := thirdPartyApis.Start
unixMilliEnd := thirdPartyApis.End
builderQueries := make(map[string]*v3.BuilderQuery)
builderQueries["endpoints"] = &v3.BuilderQuery{
QueryName: "endpoints",
DataSource: v3.DataSourceTraces,
StepInterval: defaultStepInterval,
AggregateOperator: v3.AggregateOperatorCount,
AggregateAttribute: v3.AttributeKey{
Key: urlPathKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
TimeAggregation: v3.TimeAggregationRate,
SpaceAggregation: v3.SpaceAggregationSum,
Filters: &v3.FilterSet{
Operator: "AND",
Items: getFilterSet([]v3.FilterItem{
{
Key: v3.AttributeKey{
Key: urlPathKey,
DataType: v3.AttributeKeyDataTypeString,
IsColumn: false,
Type: v3.AttributeKeyTypeTag,
},
Operator: v3.FilterOperatorExists,
Value: "",
},
{
Key: v3.AttributeKey{
Key: "kind_string",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: true,
},
Operator: "=",
Value: "Client",
},
}, thirdPartyApis.Filters),
},
Expression: "endpoints",
Disabled: false,
GroupBy: getGroupBy([]v3.AttributeKey{
{
Key: urlPathKey,
DataType: v3.AttributeKeyDataTypeString,
Type: v3.AttributeKeyTypeTag,
},
}, thirdPartyApis.GroupBy),
Legend: "",
ReduceTo: v3.ReduceToOperatorAvg,
}
builderQueries["p99"] = &v3.BuilderQuery{
QueryName: "p99",
DataSource: v3.DataSourceTraces,
StepInterval: defaultStepInterval,
AggregateOperator: v3.AggregateOperatorP99,
AggregateAttribute: v3.AttributeKey{
Key: "duration_nano",
DataType: v3.AttributeKeyDataTypeFloat64,
IsColumn: true,
},
TimeAggregation: "p99",
SpaceAggregation: v3.SpaceAggregationSum,
Filters: &v3.FilterSet{
Operator: "AND",
Items: getFilterSet([]v3.FilterItem{
{
Key: v3.AttributeKey{
Key: urlPathKey,
DataType: v3.AttributeKeyDataTypeString,
IsColumn: false,
Type: v3.AttributeKeyTypeTag,
},
Operator: v3.FilterOperatorExists,
Value: "",
},
{
Key: v3.AttributeKey{
Key: "kind_string",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: true,
},
Operator: "=",
Value: "Client",
},
}, thirdPartyApis.Filters),
},
Expression: "p99",
Disabled: false,
Having: nil,
GroupBy: getGroupBy([]v3.AttributeKey{}, thirdPartyApis.GroupBy),
Legend: "",
ReduceTo: v3.ReduceToOperatorAvg,
}
builderQueries["error_rate"] = &v3.BuilderQuery{
QueryName: "error_rate",
DataSource: v3.DataSourceTraces,
StepInterval: defaultStepInterval,
AggregateOperator: v3.AggregateOperatorRate,
AggregateAttribute: v3.AttributeKey{
Key: "",
},
TimeAggregation: v3.TimeAggregationRate,
SpaceAggregation: v3.SpaceAggregationSum,
Filters: &v3.FilterSet{
Operator: "AND",
Items: getFilterSet([]v3.FilterItem{
{
Key: v3.AttributeKey{
Key: urlPathKey,
DataType: v3.AttributeKeyDataTypeString,
IsColumn: false,
Type: v3.AttributeKeyTypeTag,
},
Operator: v3.FilterOperatorExists,
Value: "",
},
{
Key: v3.AttributeKey{
Key: "kind_string",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: true,
},
Operator: "=",
Value: "Client",
},
}, thirdPartyApis.Filters),
},
Expression: "error_rate",
Disabled: false,
GroupBy: getGroupBy([]v3.AttributeKey{}, thirdPartyApis.GroupBy),
Legend: "",
ReduceTo: v3.ReduceToOperatorAvg,
}
builderQueries["lastseen"] = &v3.BuilderQuery{
QueryName: "lastseen",
DataSource: v3.DataSourceTraces,
StepInterval: defaultStepInterval,
AggregateOperator: v3.AggregateOperatorMax,
AggregateAttribute: v3.AttributeKey{
Key: "timestamp",
},
TimeAggregation: v3.TimeAggregationMax,
SpaceAggregation: v3.SpaceAggregationSum,
Filters: &v3.FilterSet{
Operator: "AND",
Items: getFilterSet([]v3.FilterItem{
{
Key: v3.AttributeKey{
Key: urlPathKey,
DataType: v3.AttributeKeyDataTypeString,
IsColumn: false,
Type: v3.AttributeKeyTypeTag,
},
Operator: v3.FilterOperatorExists,
Value: "",
},
{
Key: v3.AttributeKey{
Key: "kind_string",
DataType: v3.AttributeKeyDataTypeString,
IsColumn: true,
},
Operator: "=",
Value: "Client",
},
}, thirdPartyApis.Filters),
},
Expression: "lastseen",
Disabled: false,
Having: nil,
OrderBy: nil,
GroupBy: getGroupBy([]v3.AttributeKey{}, thirdPartyApis.GroupBy),
Legend: "",
ReduceTo: v3.ReduceToOperatorAvg,
}
compositeQuery := &v3.CompositeQuery{
QueryType: v3.QueryTypeBuilder,
PanelType: v3.PanelTypeTable,
FillGaps: false,
BuilderQueries: builderQueries,
}
queryRangeParams := &v3.QueryRangeParamsV3{
Start: unixMilliStart,
End: unixMilliEnd,
Step: defaultStepInterval,
CompositeQuery: compositeQuery,
Version: "v4",
FormatForWeb: true,
}
return queryRangeParams, nil
}

View File

@@ -0,0 +1,267 @@
package thirdPartyApi
import (
"testing"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/stretchr/testify/assert"
)
func TestFilterResponse(t *testing.T) {
tests := []struct {
name string
input []*v3.Result
expected []*v3.Result
}{
{
name: "should filter out IP addresses from net.peer.name",
input: []*v3.Result{
{
Table: &v3.Table{
Rows: []*v3.TableRow{
{
Data: map[string]interface{}{
"net.peer.name": "192.168.1.1",
},
},
{
Data: map[string]interface{}{
"net.peer.name": "example.com",
},
},
},
},
},
},
expected: []*v3.Result{
{
Table: &v3.Table{
Rows: []*v3.TableRow{
{
Data: map[string]interface{}{
"net.peer.name": "example.com",
},
},
},
},
},
},
},
{
name: "should handle nil data",
input: []*v3.Result{
{
Table: &v3.Table{
Rows: []*v3.TableRow{
{
Data: nil,
},
},
},
},
},
expected: []*v3.Result{
{
Table: &v3.Table{
Rows: []*v3.TableRow{
{
Data: nil,
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := FilterResponse(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestGetFilterSet(t *testing.T) {
tests := []struct {
name string
existingFilters []v3.FilterItem
apiFilters v3.FilterSet
expected []v3.FilterItem
}{
{
name: "should append new filters",
existingFilters: []v3.FilterItem{
{
Key: v3.AttributeKey{Key: "existing"},
},
},
apiFilters: v3.FilterSet{
Items: []v3.FilterItem{
{
Key: v3.AttributeKey{Key: "new"},
},
},
},
expected: []v3.FilterItem{
{
Key: v3.AttributeKey{Key: "existing"},
},
{
Key: v3.AttributeKey{Key: "new"},
},
},
},
{
name: "should handle empty api filters",
existingFilters: []v3.FilterItem{{Key: v3.AttributeKey{Key: "existing"}}},
apiFilters: v3.FilterSet{},
expected: []v3.FilterItem{{Key: v3.AttributeKey{Key: "existing"}}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getFilterSet(tt.existingFilters, tt.apiFilters)
assert.Equal(t, tt.expected, result)
})
}
}
func TestGetGroupBy(t *testing.T) {
tests := []struct {
name string
existingGroup []v3.AttributeKey
apiGroup []v3.AttributeKey
expected []v3.AttributeKey
}{
{
name: "should append new group by attributes",
existingGroup: []v3.AttributeKey{
{Key: "existing"},
},
apiGroup: []v3.AttributeKey{
{Key: "new"},
},
expected: []v3.AttributeKey{
{Key: "existing"},
{Key: "new"},
},
},
{
name: "should handle empty api group",
existingGroup: []v3.AttributeKey{{Key: "existing"}},
apiGroup: []v3.AttributeKey{},
expected: []v3.AttributeKey{{Key: "existing"}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getGroupBy(tt.existingGroup, tt.apiGroup)
assert.Equal(t, tt.expected, result)
})
}
}
func TestBuildDomainList(t *testing.T) {
tests := []struct {
name string
input *ThirdPartyApis
wantErr bool
}{
{
name: "basic domain list query",
input: &ThirdPartyApis{
Start: 1000,
End: 2000,
},
wantErr: false,
},
{
name: "with filters and group by",
input: &ThirdPartyApis{
Start: 1000,
End: 2000,
Filters: v3.FilterSet{
Items: []v3.FilterItem{
{
Key: v3.AttributeKey{Key: "test"},
},
},
},
GroupBy: []v3.AttributeKey{
{Key: "test"},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := BuildDomainList(tt.input)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, tt.input.Start, result.Start)
assert.Equal(t, tt.input.End, result.End)
assert.NotNil(t, result.CompositeQuery)
assert.NotNil(t, result.CompositeQuery.BuilderQueries)
})
}
}
func TestBuildDomainInfo(t *testing.T) {
tests := []struct {
name string
input *ThirdPartyApis
wantErr bool
}{
{
name: "basic domain info query",
input: &ThirdPartyApis{
Start: 1000,
End: 2000,
},
wantErr: false,
},
{
name: "with filters and group by",
input: &ThirdPartyApis{
Start: 1000,
End: 2000,
Filters: v3.FilterSet{
Items: []v3.FilterItem{
{
Key: v3.AttributeKey{Key: "test"},
},
},
},
GroupBy: []v3.AttributeKey{
{Key: "test"},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := BuildDomainInfo(tt.input)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, tt.input.Start, result.Start)
assert.Equal(t, tt.input.End, result.End)
assert.NotNil(t, result.CompositeQuery)
assert.NotNil(t, result.CompositeQuery.BuilderQueries)
})
}
}

View File

@@ -6,7 +6,6 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/SigNoz/signoz/pkg/types/thirdpartyapitypes"
"math"
"net/http"
"sort"
@@ -15,15 +14,16 @@ import (
"text/template"
"time"
"github.com/SigNoz/govaluate"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations/messagingQueues/kafka"
queues2 "github.com/SigNoz/signoz/pkg/query-service/app/integrations/messagingQueues/queues"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations/thirdPartyApi"
"github.com/SigNoz/govaluate"
"github.com/gorilla/mux"
promModel "github.com/prometheus/common/model"
"go.uber.org/multierr"
"go.uber.org/zap"
errorsV2 "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/app/metrics"
"github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder"
"github.com/SigNoz/signoz/pkg/query-service/common"
@@ -981,15 +981,10 @@ func ParseQueueBody(r *http.Request) (*queues2.QueueListRequest, *model.ApiError
}
// ParseRequestBody for third party APIs
func ParseRequestBody(r *http.Request) (*thirdpartyapitypes.ThirdPartyApiRequest, error) {
req := new(thirdpartyapitypes.ThirdPartyApiRequest)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, "cannot parse the request body: %v", err)
func ParseRequestBody(r *http.Request) (*thirdPartyApi.ThirdPartyApis, *model.ApiError) {
thirdPartApis := new(thirdPartyApi.ThirdPartyApis)
if err := json.NewDecoder(r.Body).Decode(thirdPartApis); err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("cannot parse the request body: %v", err)}
}
if err := req.Validate(); err != nil {
return nil, err
}
return req, nil
return thirdPartApis, nil
}

View File

@@ -164,7 +164,6 @@ func (b *resourceFilterStatementBuilder[T]) addConditions(
FullTextColumn: b.fullTextColumn,
JsonBodyPrefix: b.jsonBodyPrefix,
JsonKeyToKey: b.jsonKeyToKey,
OnlyResourceFilter: true, // Only process resource terms
SkipFullTextFilter: true,
SkipFunctionCalls: true,
// there is no need for "key" not found error for resource filtering

View File

@@ -18,199 +18,6 @@ import (
var searchTroubleshootingGuideURL = "https://signoz.io/docs/userguide/search-troubleshooting/"
// BooleanExpression represents a boolean expression with proper evaluation context
type BooleanExpression struct {
SQL string
IsTrue bool
IsEmpty bool
}
// NewBooleanExpression creates a BooleanExpression from SQL
func NewBooleanExpression(sql string) BooleanExpression {
return BooleanExpression{
SQL: sql,
IsTrue: sql == "true",
IsEmpty: sql == "",
}
}
// booleanEvaluatingVisitor is a specialized visitor for resource filter context
// that properly applies boolean algebra during tree traversal
type booleanEvaluatingVisitor struct {
*filterExpressionVisitor
}
func newBooleanEvaluatingVisitor(opts FilterExprVisitorOpts) *booleanEvaluatingVisitor {
return &booleanEvaluatingVisitor{
filterExpressionVisitor: newFilterExpressionVisitor(opts),
}
}
// Visit dispatches to boolean-aware visit methods
func (v *booleanEvaluatingVisitor) Visit(tree antlr.ParseTree) any {
if tree == nil {
return NewBooleanExpression("")
}
switch t := tree.(type) {
case *grammar.QueryContext:
return v.VisitQuery(t)
case *grammar.ExpressionContext:
return v.VisitExpression(t)
case *grammar.OrExpressionContext:
return v.VisitOrExpression(t)
case *grammar.AndExpressionContext:
return v.VisitAndExpression(t)
case *grammar.UnaryExpressionContext:
return v.VisitUnaryExpression(t)
case *grammar.PrimaryContext:
return v.VisitPrimary(t)
default:
// For leaf nodes, delegate to original visitor and wrap result
result := v.filterExpressionVisitor.Visit(tree)
if sql, ok := result.(string); ok {
return NewBooleanExpression(sql)
}
return NewBooleanExpression("")
}
}
func (v *booleanEvaluatingVisitor) VisitQuery(ctx *grammar.QueryContext) any {
return v.Visit(ctx.Expression())
}
func (v *booleanEvaluatingVisitor) VisitExpression(ctx *grammar.ExpressionContext) any {
return v.Visit(ctx.OrExpression())
}
func (v *booleanEvaluatingVisitor) VisitOrExpression(ctx *grammar.OrExpressionContext) any {
andExpressions := ctx.AllAndExpression()
var result BooleanExpression
hasTrue := false
hasEmpty := false
for i, expr := range andExpressions {
exprResult := v.Visit(expr).(BooleanExpression)
if exprResult.IsTrue {
hasTrue = true
}
if exprResult.IsEmpty {
hasEmpty = true
}
if i == 0 {
result = exprResult
} else {
if result.IsEmpty {
result = exprResult
} else if !exprResult.IsEmpty {
sql := v.builder.Or(result.SQL, exprResult.SQL)
result = NewBooleanExpression(sql)
}
}
}
// In resource filter context, if any operand is empty (meaning "include all resources"),
// the entire OR should be empty (include all resources)
if hasEmpty && v.onlyResourceFilter {
result.IsEmpty = true
result.IsTrue = true
result.SQL = ""
} else if hasTrue {
// Mark as always true if any operand is true, but preserve the SQL structure
result.IsTrue = true
}
return result
}
func (v *booleanEvaluatingVisitor) VisitAndExpression(ctx *grammar.AndExpressionContext) any {
unaryExpressions := ctx.AllUnaryExpression()
var result BooleanExpression
allTrue := true
for i, expr := range unaryExpressions {
exprResult := v.Visit(expr).(BooleanExpression)
if !exprResult.IsTrue && !exprResult.IsEmpty {
allTrue = false
}
if i == 0 {
result = exprResult
} else {
// Apply boolean AND logic
if exprResult.IsTrue {
// A AND true = A, continue with result
continue
}
if result.IsTrue {
result = exprResult
} else if result.IsEmpty {
result = exprResult
} else if !exprResult.IsEmpty {
sql := v.builder.And(result.SQL, exprResult.SQL)
result = NewBooleanExpression(sql)
}
}
}
// If all terms were "true", mark the result as always true
if allTrue && len(unaryExpressions) > 0 {
result.IsTrue = true
if result.SQL == "" {
result.SQL = "true"
}
}
return result
}
func (v *booleanEvaluatingVisitor) VisitUnaryExpression(ctx *grammar.UnaryExpressionContext) any {
result := v.Visit(ctx.Primary()).(BooleanExpression)
if ctx.NOT() != nil {
// Apply NOT logic with resource filter context awareness
if v.onlyResourceFilter {
if result.IsTrue {
return NewBooleanExpression("") // NOT(true) = include all resources
}
if result.IsEmpty {
return NewBooleanExpression("") // NOT(empty) = include all resources
}
}
sql := fmt.Sprintf("NOT (%s)", result.SQL)
return NewBooleanExpression(sql)
}
return result
}
func (v *booleanEvaluatingVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any {
if ctx.OrExpression() != nil {
result := v.Visit(ctx.OrExpression()).(BooleanExpression)
// If no boolean simplification happened, preserve original parentheses structure
if !result.IsEmpty && !result.IsTrue {
// Use original visitor to get proper parentheses structure
originalSQL := v.filterExpressionVisitor.Visit(ctx)
if sql, ok := originalSQL.(string); ok && sql != "" {
return NewBooleanExpression(sql)
}
result.SQL = fmt.Sprintf("(%s)", result.SQL)
}
return result
}
// For other cases, delegate to original visitor
sqlResult := v.filterExpressionVisitor.Visit(ctx)
if sql, ok := sqlResult.(string); ok {
return NewBooleanExpression(sql)
}
return NewBooleanExpression("")
}
// filterExpressionVisitor implements the FilterQueryVisitor interface
// to convert the parsed filter expressions into ClickHouse WHERE clause
type filterExpressionVisitor struct {
@@ -227,7 +34,6 @@ type filterExpressionVisitor struct {
jsonBodyPrefix string
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
skipResourceFilter bool
onlyResourceFilter bool
skipFullTextFilter bool
skipFunctionCalls bool
ignoreNotFoundKeys bool
@@ -246,7 +52,6 @@ type FilterExprVisitorOpts struct {
JsonBodyPrefix string
JsonKeyToKey qbtypes.JsonKeyToFieldFunc
SkipResourceFilter bool
OnlyResourceFilter bool // Only process resource terms, skip non-resource terms
SkipFullTextFilter bool
SkipFunctionCalls bool
IgnoreNotFoundKeys bool
@@ -265,7 +70,6 @@ func newFilterExpressionVisitor(opts FilterExprVisitorOpts) *filterExpressionVis
jsonBodyPrefix: opts.JsonBodyPrefix,
jsonKeyToKey: opts.JsonKeyToKey,
skipResourceFilter: opts.SkipResourceFilter,
onlyResourceFilter: opts.OnlyResourceFilter,
skipFullTextFilter: opts.SkipFullTextFilter,
skipFunctionCalls: opts.SkipFunctionCalls,
ignoreNotFoundKeys: opts.IgnoreNotFoundKeys,
@@ -291,6 +95,8 @@ func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (*PreparedWher
opts.Builder = sb
}
visitor := newFilterExpressionVisitor(opts)
// Set up error handling
lexerErrorListener := NewErrorListener()
lexer.RemoveErrorListeners()
@@ -305,17 +111,6 @@ func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (*PreparedWher
// Parse the query
tree := parser.Query()
// override skipResourceFilter if the expression contains OR
for _, tok := range tokens.GetAllTokens() {
if tok.GetTokenType() == grammar.FilterQueryParserOR {
opts.SkipResourceFilter = false
break
}
}
tokens.Reset()
visitor := newFilterExpressionVisitor(opts)
// Handle syntax errors
if len(parserErrorListener.SyntaxErrors) > 0 {
combinedErrors := errors.Newf(
@@ -356,31 +151,6 @@ func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (*PreparedWher
cond = "true"
}
// In resource filter context, apply robust boolean evaluation only when needed
if opts.OnlyResourceFilter {
// Check if the condition contains patterns that need boolean simplification
// We need boolean evaluation when:
// 1. Expression contains " true" (indicating simplified non-resource terms)
// 2. Expression is exactly "true"
// 3. Expression contains "NOT" with true values that need simplification
needsBooleanEval := strings.Contains(cond, " true") ||
cond == "true" ||
(strings.Contains(cond, "NOT") && strings.Contains(cond, "true"))
if needsBooleanEval {
// Re-parse and evaluate with boolean algebra
boolVisitor := newBooleanEvaluatingVisitor(opts)
boolResult := boolVisitor.Visit(tree)
if boolExpr, ok := boolResult.(BooleanExpression); ok {
if boolExpr.IsEmpty {
cond = "true" // Empty means include all resources
} else {
cond = boolExpr.SQL
}
}
}
}
whereClause := sqlbuilder.NewWhereClause().AddWhereExpr(visitor.builder.Args, cond)
return &PreparedWhereClause{whereClause, visitor.warnings, visitor.mainWarnURL}, nil
@@ -447,23 +217,22 @@ func (v *filterExpressionVisitor) VisitExpression(ctx *grammar.ExpressionContext
func (v *filterExpressionVisitor) VisitOrExpression(ctx *grammar.OrExpressionContext) any {
andExpressions := ctx.AllAndExpression()
validConditions := []string{}
for _, expr := range andExpressions {
andExpressionConditions := make([]string, len(andExpressions))
for i, expr := range andExpressions {
if condExpr, ok := v.Visit(expr).(string); ok && condExpr != "" {
validConditions = append(validConditions, condExpr)
andExpressionConditions[i] = condExpr
}
}
if len(validConditions) == 0 {
if len(andExpressionConditions) == 0 {
return ""
}
if len(validConditions) == 1 {
return validConditions[0]
if len(andExpressionConditions) == 1 {
return andExpressionConditions[0]
}
return v.builder.Or(validConditions...)
return v.builder.Or(andExpressionConditions...)
}
// VisitAndExpression handles AND expressions
@@ -494,17 +263,6 @@ func (v *filterExpressionVisitor) VisitUnaryExpression(ctx *grammar.UnaryExpress
// Check if this is a NOT expression
if ctx.NOT() != nil {
// In resource filter context, handle NOT specially
if v.onlyResourceFilter {
// NOT(true) means NOT(all non-resource terms) which means "include all resources"
if result == "true" {
return "" // No filtering = include all resources
}
// NOT(empty) should return empty (no filtering)
if result == "" {
return ""
}
}
return fmt.Sprintf("NOT (%s)", result)
}
@@ -516,7 +274,7 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
if ctx.OrExpression() != nil {
// This is a parenthesized expression
if condExpr, ok := v.Visit(ctx.OrExpression()).(string); ok && condExpr != "" {
return fmt.Sprintf("(%s)", condExpr)
return fmt.Sprintf("(%s)", v.Visit(ctx.OrExpression()).(string))
}
return ""
} else if ctx.Comparison() != nil {
@@ -598,22 +356,6 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext
}
}
// this is used to only process resource terms in resource filter context
if v.onlyResourceFilter {
filteredKeys := []*telemetrytypes.TelemetryFieldKey{}
for _, key := range keys {
if key.FieldContext == telemetrytypes.FieldContextResource {
filteredKeys = append(filteredKeys, key)
}
}
keys = filteredKeys
if len(keys) == 0 {
// For non-resource terms in resource filter context, return "true"
// This ensures OR expressions work correctly (e.g., resource OR non-resource)
return "true"
}
}
// Handle EXISTS specially
if ctx.EXISTS() != nil {
op := qbtypes.FilterOperatorExists

View File

@@ -74,35 +74,6 @@ func TestStatementBuilderTimeSeries(t *testing.T) {
},
expectedErr: nil,
},
{
name: "Time series with OR b/w resource attr and attribute filter",
requestType: qbtypes.RequestTypeTimeSeries,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Aggregations: []qbtypes.LogAggregation{
{
Expression: "count()",
},
},
Filter: &qbtypes.Filter{
Expression: "service.name = 'redis-manual' OR http.method = 'GET'",
},
Limit: 10,
GroupBy: []qbtypes.GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
},
},
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 SECOND) AS ts, toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "redis-manual", true, "GET", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 10, true, "redis-manual", true, "GET", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
},
expectedErr: nil,
},
{
name: "Time series with limit + custom order by",
requestType: qbtypes.RequestTypeTimeSeries,
@@ -142,111 +113,6 @@ func TestStatementBuilderTimeSeries(t *testing.T) {
},
expectedErr: nil,
},
{
name: "Time series with NOT predicate containing only non-resource terms",
requestType: qbtypes.RequestTypeTimeSeries,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
Aggregations: []qbtypes.LogAggregation{
{
Expression: "count()",
},
},
Filter: &qbtypes.Filter{
Expression: "NOT (message CONTAINS 'foo' AND hasToken(body, 'bar'))",
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND NOT ((((LOWER(attributes_string['message']) LIKE LOWER(?) AND mapContains(attributes_string, 'message') = ?) AND hasToken(LOWER(body), LOWER(?))))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY ts",
Args: []any{uint64(1747945619), uint64(1747983448), "%foo%", true, "bar", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
},
expectedErr: nil,
},
{
name: "Time series with NOT OR mixed resource/non-resource terms",
requestType: qbtypes.RequestTypeTimeSeries,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
Aggregations: []qbtypes.LogAggregation{
{
Expression: "count()",
},
},
Filter: &qbtypes.Filter{
Expression: "NOT (service.name = 'redis-manual' OR http.method = 'GET')",
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND NOT ((((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (attributes_string['http.method'] = ? AND mapContains(attributes_string, 'http.method') = ?)))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY ts",
Args: []any{uint64(1747945619), uint64(1747983448), "redis-manual", true, "GET", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
},
expectedErr: nil,
},
{
name: "Time series with complex NOT expression and nested OR conditions",
requestType: qbtypes.RequestTypeTimeSeries,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
Aggregations: []qbtypes.LogAggregation{
{
Expression: "count()",
},
},
Filter: &qbtypes.Filter{
Expression: "service.name IN 'redis' AND request.type = 'External' AND http.status_code < 500 AND http.status_code >= 400 AND NOT ((http.request.header.tenant_id = '[\"tenant-1\"]' AND http.status_code = 401) OR (http.request.header.tenant_id = '[\"tenant-2\"]' AND http.status_code = 404 AND http.route = '/tenants/{tenant_id}'))",
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ?) AND labels LIKE ? AND (labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((resources_string['service.name'] = ?) AND mapContains(resources_string, 'service.name') = ?) AND (attributes_string['request.type'] = ? AND mapContains(attributes_string, 'request.type') = ?) AND (toFloat64(attributes_number['http.status_code']) < ? AND mapContains(attributes_number, 'http.status_code') = ?) AND (toFloat64(attributes_number['http.status_code']) >= ? AND mapContains(attributes_number, 'http.status_code') = ?) AND NOT ((((((attributes_string['http.request.header.tenant_id'] = ? AND mapContains(attributes_string, 'http.request.header.tenant_id') = ?) AND (toFloat64(attributes_number['http.status_code']) = ? AND mapContains(attributes_number, 'http.status_code') = ?))) OR (((attributes_string['http.request.header.tenant_id'] = ? AND mapContains(attributes_string, 'http.request.header.tenant_id') = ?) AND (toFloat64(attributes_number['http.status_code']) = ? AND mapContains(attributes_number, 'http.status_code') = ?) AND (attributes_string['http.route'] = ? AND mapContains(attributes_string, 'http.route') = ?))))))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY ts",
Args: []any{"redis", "%service.name%", "%service.name\":\"redis%", uint64(1747945619), uint64(1747983448), "redis", true, "External", true, float64(500), true, float64(400), true, "[\"tenant-1\"]", true, float64(401), true, "[\"tenant-2\"]", true, float64(404), true, "/tenants/{tenant_id}", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
},
expectedErr: nil,
},
{
name: "Time series with complex OR expression containing NOT with nested conditions",
requestType: qbtypes.RequestTypeTimeSeries,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
Aggregations: []qbtypes.LogAggregation{
{
Expression: "count()",
},
},
Filter: &qbtypes.Filter{
Expression: "(service.name IN 'redis' AND request.type = 'External' AND http.status_code < 500 AND http.status_code >= 400 OR NOT ((http.request.header.tenant_id = '[\"tenant-1\"]' AND http.status_code = 401) OR (http.request.header.tenant_id = '[\"tenant-2\"]' AND http.status_code = 404 AND http.route = '/tenants/{tenant_id}')))",
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE true AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (((((resources_string['service.name'] = ?) AND mapContains(resources_string, 'service.name') = ?) AND (attributes_string['request.type'] = ? AND mapContains(attributes_string, 'request.type') = ?) AND (toFloat64(attributes_number['http.status_code']) < ? AND mapContains(attributes_number, 'http.status_code') = ?) AND (toFloat64(attributes_number['http.status_code']) >= ? AND mapContains(attributes_number, 'http.status_code') = ?)) OR NOT ((((((attributes_string['http.request.header.tenant_id'] = ? AND mapContains(attributes_string, 'http.request.header.tenant_id') = ?) AND (toFloat64(attributes_number['http.status_code']) = ? AND mapContains(attributes_number, 'http.status_code') = ?))) OR (((attributes_string['http.request.header.tenant_id'] = ? AND mapContains(attributes_string, 'http.request.header.tenant_id') = ?) AND (toFloat64(attributes_number['http.status_code']) = ? AND mapContains(attributes_number, 'http.status_code') = ?) AND (attributes_string['http.route'] = ? AND mapContains(attributes_string, 'http.route') = ?)))))))) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY ts",
Args: []any{uint64(1747945619), uint64(1747983448), "redis", true, "External", true, float64(500), true, float64(400), true, "[\"tenant-1\"]", true, float64(401), true, "[\"tenant-2\"]", true, float64(404), true, "/tenants/{tenant_id}", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
},
expectedErr: nil,
},
{
name: "Time series with OR between multiple resource conditions",
requestType: qbtypes.RequestTypeTimeSeries,
query: qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
Aggregations: []qbtypes.LogAggregation{
{
Expression: "count()",
},
},
Filter: &qbtypes.Filter{
Expression: "service.name = 'redis' OR service.name = 'driver'",
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_logs.distributed_logs_v2_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, count() AS __result_0 FROM signoz_logs.distributed_logs_v2 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY ts",
Args: []any{"redis", "%service.name%", "%service.name\":\"redis%", "driver", "%service.name%", "%service.name\":\"driver%", uint64(1747945619), uint64(1747983448), "redis", true, "driver", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
},
expectedErr: nil,
},
}
fm := NewFieldMapper()

View File

@@ -862,27 +862,6 @@ func buildCompleteFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
Materialized: true,
},
},
"request.type": {
{
Name: "request.type",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
"http.request.header.tenant_id": {
{
Name: "http.request.header.tenant_id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
"http.route": {
{
Name: "http.route",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
}
for _, keys := range keysMap {

View File

@@ -89,8 +89,8 @@ func TestStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((resources_string['service.name'] = ? AND mapContains(resources_string, 'service.name') = ?) OR (attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "redis-manual", true, "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, true, "redis-manual", true, "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE ((simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) OR true) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __limit_cte AS (SELECT toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(mapContains(resources_string, 'service.name') = ?, resources_string['service.name'], NULL)) AS `service.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND ((attributes_string['http.request.method'] = ? AND mapContains(attributes_string, 'http.request.method') = ?)) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, true, "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
},
expectedErr: nil,
},

View File

@@ -145,25 +145,6 @@ func (q *QueryBuilderQuery[T]) Validate(requestType RequestType) error {
}
}
if requestType == RequestTypeRaw {
if err := q.validateSelectFields(); err != nil {
return err
}
}
return nil
}
func (q *QueryBuilderQuery[T]) validateSelectFields() error {
// isRoot and isEntryPoint are returned by the Metadata API, so if someone sends them, we have to reject the request.
for _, v := range q.SelectFields {
if v.Name == "isRoot" || v.Name == "isEntryPoint" {
return errors.NewInvalidInputf(
errors.CodeInvalidInput,
"isRoot and isEntryPoint fields are not supported in selectFields",
)
}
}
return nil
}

View File

@@ -1,29 +0,0 @@
package thirdpartyapitypes
import (
"github.com/SigNoz/signoz/pkg/errors"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
)
type ThirdPartyApiRequest struct {
Start uint64 `json:"start"`
End uint64 `json:"end"`
ShowIp bool `json:"show_ip,omitempty"`
Domain string `json:"domain,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
Filter *qbtypes.Filter `json:"filters,omitempty"`
GroupBy []qbtypes.GroupByKey `json:"groupBy,omitempty"`
}
// Validate validates the ThirdPartyApiRequest
func (req *ThirdPartyApiRequest) Validate() error {
if req.Start >= req.End {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "start time must be before end time")
}
if req.Filter != nil && req.Filter.Expression == "" {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "filter expression cannot be empty when filter is provided")
}
return nil
}

View File

@@ -1,8 +1,9 @@
<!DOCTYPE html>
<html>
<body>
Hi {{.CustomerName}},<br>
Your role in <strong>SigNoz</strong> has been updated by {{.UpdatedByEmail}}.
<p>Hi {{.CustomerName}},</p>
<p>We wanted to inform you that your role in the <strong>SigNoz</strong> project has been updated by <strong>{{.UpdatedByEmail}}</strong>.</p>
<p>
<strong>Previous Role:</strong> {{.OldRole}}<br>
@@ -10,19 +11,13 @@
</p>
<p>
Please note that you will need to log out and log back in for the changes to take effect.
Please note that you will need to <strong>log out and log back in</strong> for the changes to take effect.
</p>
{{if eq .OldRole "Admin"}}
<p>
If you were not expecting this change or have any questions, please contact us at <a href="mailto:support@signoz.io">support@signoz.io</a>.
</p>
{{else}}
<p>
If you were not expecting this change or have any questions, please reach out to your administrator.
</p>
{{end}}
<p>
If you were not expecting this change or have any questions, please reach out to your project administrator or contact us at <a href="mailto:support@signoz.io">support@signoz.io</a>.
</p>
<p>Best regards,<br>The SigNoz Team</p>
<p>Thanks,<br/>The SigNoz Team</p>
</body>
</html>