Compare commits
9 Commits
testing-qb
...
chore/test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
700249fae7 | ||
|
|
bed3dbc698 | ||
|
|
66affb0ece | ||
|
|
75f62372ae | ||
|
|
a3ac307b4e | ||
|
|
7672d2f636 | ||
|
|
e3018d9529 | ||
|
|
385ee268e3 | ||
|
|
01036a8a2f |
@@ -5,6 +5,8 @@
|
||||
<br>SigNoz
|
||||
</h1>
|
||||
|
||||
ok
|
||||
|
||||
<p align="center">All your logs, metrics, and traces in one place. Monitor your application, spot issues before they occur and troubleshoot downtime quickly with rich context. SigNoz is a cost-effective open-source alternative to Datadog and New Relic. Visit <a href="https://signoz.io" target="_blank">signoz.io</a> for the full documentation, tutorials, and guide.</p>
|
||||
|
||||
<p align="center">
|
||||
|
||||
@@ -224,3 +224,6 @@ statsreporter:
|
||||
enabled: true
|
||||
# The interval at which the stats are collected.
|
||||
interval: 6h
|
||||
collect:
|
||||
# Whether to collect identities and traits (emails).
|
||||
identities: true
|
||||
|
||||
@@ -119,6 +119,7 @@ export const updateFunnelSteps = async (
|
||||
export interface ValidateFunnelPayload {
|
||||
start_time: number;
|
||||
end_time: number;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
export interface ValidateFunnelResponse {
|
||||
@@ -132,12 +133,11 @@ export interface ValidateFunnelResponse {
|
||||
}
|
||||
|
||||
export const validateFunnelSteps = async (
|
||||
funnelId: string,
|
||||
payload: ValidateFunnelPayload,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<ValidateFunnelResponse> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/validate`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/validate`,
|
||||
payload,
|
||||
{ signal },
|
||||
);
|
||||
@@ -185,6 +185,7 @@ export interface FunnelOverviewPayload {
|
||||
end_time: number;
|
||||
step_start?: number;
|
||||
step_end?: number;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
export interface FunnelOverviewResponse {
|
||||
@@ -202,12 +203,11 @@ export interface FunnelOverviewResponse {
|
||||
}
|
||||
|
||||
export const getFunnelOverview = async (
|
||||
funnelId: string,
|
||||
payload: FunnelOverviewPayload,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<FunnelOverviewResponse> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/overview`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/overview`,
|
||||
payload,
|
||||
{
|
||||
signal,
|
||||
@@ -235,12 +235,11 @@ export interface SlowTraceData {
|
||||
}
|
||||
|
||||
export const getFunnelSlowTraces = async (
|
||||
funnelId: string,
|
||||
payload: FunnelOverviewPayload,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<SlowTraceData> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/slow-traces`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/slow-traces`,
|
||||
payload,
|
||||
{
|
||||
signal,
|
||||
@@ -273,7 +272,7 @@ export const getFunnelErrorTraces = async (
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<ErrorTraceData> | ErrorResponse> => {
|
||||
const response: AxiosResponse = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/error-traces`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/error-traces`,
|
||||
payload,
|
||||
{
|
||||
signal,
|
||||
@@ -291,6 +290,7 @@ export const getFunnelErrorTraces = async (
|
||||
export interface FunnelStepsPayload {
|
||||
start_time: number;
|
||||
end_time: number;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
export interface FunnelStepGraphMetrics {
|
||||
@@ -307,12 +307,11 @@ export interface FunnelStepsResponse {
|
||||
}
|
||||
|
||||
export const getFunnelSteps = async (
|
||||
funnelId: string,
|
||||
payload: FunnelStepsPayload,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<FunnelStepsResponse> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/steps`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/steps`,
|
||||
payload,
|
||||
{ signal },
|
||||
);
|
||||
@@ -330,6 +329,7 @@ export interface FunnelStepsOverviewPayload {
|
||||
end_time: number;
|
||||
step_start?: number;
|
||||
step_end?: number;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
export interface FunnelStepsOverviewResponse {
|
||||
@@ -341,12 +341,11 @@ export interface FunnelStepsOverviewResponse {
|
||||
}
|
||||
|
||||
export const getFunnelStepsOverview = async (
|
||||
funnelId: string,
|
||||
payload: FunnelStepsOverviewPayload,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<FunnelStepsOverviewResponse> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/steps/overview`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/steps/overview`,
|
||||
payload,
|
||||
{ signal },
|
||||
);
|
||||
|
||||
@@ -30,5 +30,5 @@ export enum LOCALSTORAGE {
|
||||
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
|
||||
BANNER_DISMISSED = 'BANNER_DISMISSED',
|
||||
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
|
||||
UNEXECUTED_FUNNELS = 'UNEXECUTED_FUNNELS',
|
||||
FUNNEL_STEPS = 'FUNNEL_STEPS',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import HostsEmptyOrIncorrectMetrics from '../HostsEmptyOrIncorrectMetrics';
|
||||
|
||||
describe('HostsEmptyOrIncorrectMetrics', () => {
|
||||
it('shows no data message when noData is true', () => {
|
||||
render(<HostsEmptyOrIncorrectMetrics noData incorrectData={false} />);
|
||||
expect(
|
||||
screen.getByText('No host metrics data received yet.'),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Infrastructure monitoring requires the/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows incorrect data message when incorrectData is true', () => {
|
||||
render(<HostsEmptyOrIncorrectMetrics noData={false} incorrectData />);
|
||||
expect(
|
||||
screen.getByText(
|
||||
'To see host metrics, upgrade to the latest version of SigNoz k8s-infra chart. Please contact support if you need help.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show no data message when noData is false', () => {
|
||||
render(<HostsEmptyOrIncorrectMetrics noData={false} incorrectData={false} />);
|
||||
expect(
|
||||
screen.queryByText('No host metrics data received yet.'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(/Infrastructure monitoring requires the/),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show incorrect data message when incorrectData is false', () => {
|
||||
render(<HostsEmptyOrIncorrectMetrics noData={false} incorrectData={false} />);
|
||||
expect(
|
||||
screen.queryByText(
|
||||
'To see host metrics, upgrade to the latest version of SigNoz k8s-infra chart. Please contact support if you need help.',
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
/* eslint-disable react/button-has-type */
|
||||
import { render } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
import * as useGetHostListHooks from 'hooks/infraMonitoring/useGetHostList';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
import * as timezoneHooks from 'providers/Timezone';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import store from 'store';
|
||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||
|
||||
import HostsList from '../HostsList';
|
||||
|
||||
jest.mock('lib/getMinMax', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => ({
|
||||
minTime: 1713734400000,
|
||||
maxTime: 1713738000000,
|
||||
isValidTimeFormat: jest.fn().mockReturnValue(true),
|
||||
})),
|
||||
}));
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
default: ({ onSelect, selectedTime, selectedValue }: any): JSX.Element => (
|
||||
<div data-testid="custom-time-picker">
|
||||
<button onClick={(): void => onSelect('custom')}>
|
||||
{selectedTime} - {selectedValue}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: (): any => ({
|
||||
globalTime: {
|
||||
selectedTime: {
|
||||
startTime: 1713734400000,
|
||||
endTime: 1713738000000,
|
||||
},
|
||||
maxTime: 1713738000000,
|
||||
minTime: 1713734400000,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn().mockReturnValue({
|
||||
pathname: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
|
||||
}),
|
||||
}));
|
||||
jest.mock('react-router-dom-v5-compat', () => {
|
||||
const actual = jest.requireActual('react-router-dom-v5-compat');
|
||||
return {
|
||||
...actual,
|
||||
useSearchParams: jest
|
||||
.fn()
|
||||
.mockReturnValue([
|
||||
{ get: jest.fn(), entries: jest.fn().mockReturnValue([]) },
|
||||
jest.fn(),
|
||||
]),
|
||||
useNavigationType: (): any => 'PUSH',
|
||||
};
|
||||
});
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): any => ({
|
||||
safeNavigate: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.spyOn(timezoneHooks, 'useTimezone').mockReturnValue({
|
||||
timezone: {
|
||||
offset: 0,
|
||||
},
|
||||
browserTimezone: {
|
||||
offset: 0,
|
||||
},
|
||||
} as any);
|
||||
jest.spyOn(useGetHostListHooks, 'useGetHostList').mockReturnValue({
|
||||
data: {
|
||||
payload: {
|
||||
data: {
|
||||
records: [
|
||||
{
|
||||
hostName: 'test-host',
|
||||
active: true,
|
||||
cpu: 0.75,
|
||||
memory: 0.65,
|
||||
wait: 0.03,
|
||||
},
|
||||
],
|
||||
isSendingK8SAgentMetrics: false,
|
||||
sentAnyHostMetricsData: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
} as any);
|
||||
jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
|
||||
user: {
|
||||
role: 'admin',
|
||||
},
|
||||
activeLicenseV3: {
|
||||
event_queue: {
|
||||
created_at: '0',
|
||||
event: LicenseEvent.NO_EVENT,
|
||||
scheduled_at: '0',
|
||||
status: '',
|
||||
updated_at: '0',
|
||||
},
|
||||
license: {
|
||||
license_key: 'test-license-key',
|
||||
license_type: 'trial',
|
||||
org_id: 'test-org-id',
|
||||
plan_id: 'test-plan-id',
|
||||
plan_name: 'test-plan-name',
|
||||
plan_type: 'trial',
|
||||
plan_version: 'test-plan-version',
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
describe('HostsList', () => {
|
||||
it('renders hosts list table', () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<HostsList />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders filters', () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<HostsList />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
expect(container.querySelector('.filters')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import HostsListControls from '../HostsListControls';
|
||||
|
||||
jest.mock('container/QueryBuilder/filters/QueryBuilderSearch', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="query-builder-search">Search</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="date-time-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('HostsListControls', () => {
|
||||
const mockHandleFiltersChange = jest.fn();
|
||||
const mockFilters = {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
it('renders search and date time filters', () => {
|
||||
render(
|
||||
<HostsListControls
|
||||
handleFiltersChange={mockHandleFiltersChange}
|
||||
filters={mockFilters}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('query-builder-search')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('date-time-selection')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import HostsListTable from '../HostsListTable';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
const EMPTY_STATE_CONTAINER_CLASS = '.hosts-empty-state-container';
|
||||
|
||||
describe('HostsListTable', () => {
|
||||
const mockHost = {
|
||||
hostName: 'test-host-1',
|
||||
active: true,
|
||||
cpu: 0.75,
|
||||
memory: 0.65,
|
||||
wait: 0.03,
|
||||
load15: 1.5,
|
||||
os: 'linux',
|
||||
};
|
||||
|
||||
const mockTableData = {
|
||||
payload: {
|
||||
data: {
|
||||
hosts: [mockHost],
|
||||
},
|
||||
},
|
||||
};
|
||||
const mockOnHostClick = jest.fn();
|
||||
const mockSetCurrentPage = jest.fn();
|
||||
const mockSetOrderBy = jest.fn();
|
||||
const mockSetPageSize = jest.fn();
|
||||
const mockProps = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
isFetching: false,
|
||||
tableData: mockTableData,
|
||||
hostMetricsData: [mockHost],
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
onHostClick: mockOnHostClick,
|
||||
currentPage: 1,
|
||||
setCurrentPage: mockSetCurrentPage,
|
||||
pageSize: 10,
|
||||
setOrderBy: mockSetOrderBy,
|
||||
setPageSize: mockSetPageSize,
|
||||
} as any;
|
||||
|
||||
it('renders loading state if isLoading is true', () => {
|
||||
const { container } = render(<HostsListTable {...mockProps} isLoading />);
|
||||
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders loading state if isFetching is true', () => {
|
||||
const { container } = render(<HostsListTable {...mockProps} isFetching />);
|
||||
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders error state if isError is true', () => {
|
||||
render(<HostsListTable {...mockProps} isError />);
|
||||
expect(screen.getByText('Something went wrong')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders empty state if no hosts are found', () => {
|
||||
const { container } = render(<HostsListTable {...mockProps} />);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders empty state if sentAnyHostMetricsData is false', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
tableData={{
|
||||
...mockTableData,
|
||||
payload: {
|
||||
...mockTableData.payload,
|
||||
data: {
|
||||
...mockTableData.payload.data,
|
||||
sentAnyHostMetricsData: false,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders empty state if isSendingIncorrectK8SAgentMetrics is true', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
tableData={{
|
||||
...mockTableData,
|
||||
payload: {
|
||||
...mockTableData.payload,
|
||||
data: {
|
||||
...mockTableData.payload.data,
|
||||
isSendingIncorrectK8SAgentMetrics: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders table data', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
tableData={{
|
||||
...mockTableData,
|
||||
payload: {
|
||||
...mockTableData.payload,
|
||||
data: {
|
||||
...mockTableData.payload.data,
|
||||
isSendingIncorrectK8SAgentMetrics: false,
|
||||
sentAnyHostMetricsData: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-table')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { formatDataForTable, GetHostsQuickFiltersConfig } from '../utils';
|
||||
|
||||
const PROGRESS_BAR_CLASS = '.progress-bar';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
describe('InfraMonitoringHosts utils', () => {
|
||||
describe('formatDataForTable', () => {
|
||||
it('should format host data correctly', () => {
|
||||
const mockData = [
|
||||
{
|
||||
hostName: 'test-host',
|
||||
active: true,
|
||||
cpu: 0.95,
|
||||
memory: 0.85,
|
||||
wait: 0.05,
|
||||
load15: 2.5,
|
||||
os: 'linux',
|
||||
},
|
||||
] as any;
|
||||
|
||||
const result = formatDataForTable(mockData);
|
||||
|
||||
expect(result[0].hostName).toBe('test-host');
|
||||
expect(result[0].wait).toBe('5%');
|
||||
expect(result[0].load15).toBe(2.5);
|
||||
|
||||
// Test active tag rendering
|
||||
const activeTag = render(result[0].active as JSX.Element);
|
||||
expect(activeTag.container.textContent).toBe('ACTIVE');
|
||||
expect(activeTag.container.querySelector('.active')).toBeTruthy();
|
||||
|
||||
// Test CPU progress bar
|
||||
const cpuProgress = render(result[0].cpu as JSX.Element);
|
||||
const cpuProgressBar = cpuProgress.container.querySelector(
|
||||
PROGRESS_BAR_CLASS,
|
||||
);
|
||||
expect(cpuProgressBar).toBeTruthy();
|
||||
|
||||
// Test memory progress bar
|
||||
const memoryProgress = render(result[0].memory as JSX.Element);
|
||||
const memoryProgressBar = memoryProgress.container.querySelector(
|
||||
PROGRESS_BAR_CLASS,
|
||||
);
|
||||
expect(memoryProgressBar).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handle inactive hosts', () => {
|
||||
const mockData = [
|
||||
{
|
||||
hostName: 'test-host',
|
||||
active: false,
|
||||
cpu: 0.3,
|
||||
memory: 0.4,
|
||||
wait: 0.02,
|
||||
load15: 1.2,
|
||||
os: 'linux',
|
||||
cpuTimeSeries: [],
|
||||
memoryTimeSeries: [],
|
||||
waitTimeSeries: [],
|
||||
load15TimeSeries: [],
|
||||
},
|
||||
] as any;
|
||||
|
||||
const result = formatDataForTable(mockData);
|
||||
|
||||
const inactiveTag = render(result[0].active as JSX.Element);
|
||||
expect(inactiveTag.container.textContent).toBe('INACTIVE');
|
||||
expect(inactiveTag.container.querySelector('.inactive')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetHostsQuickFiltersConfig', () => {
|
||||
it('should return correct config when dotMetricsEnabled is true', () => {
|
||||
const result = GetHostsQuickFiltersConfig(true);
|
||||
|
||||
expect(result[0].attributeKey.key).toBe('host.name');
|
||||
expect(result[1].attributeKey.key).toBe('os.type');
|
||||
expect(result[0].aggregateAttribute).toBe('system.cpu.load_average.15m');
|
||||
});
|
||||
|
||||
it('should return correct config when dotMetricsEnabled is false', () => {
|
||||
const result = GetHostsQuickFiltersConfig(false);
|
||||
|
||||
expect(result[0].attributeKey.key).toBe('host_name');
|
||||
expect(result[1].attributeKey.key).toBe('os_type');
|
||||
expect(result[0].aggregateAttribute).toBe('system_cpu_load_average_15m');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -611,9 +611,7 @@ export const errorPercentage = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.StatusCodeNorm,
|
||||
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
|
||||
dataType: DataTypes.Int64,
|
||||
isColumn: false,
|
||||
type: MetricsType.Tag,
|
||||
|
||||
@@ -241,6 +241,15 @@
|
||||
&-title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
&-footer {
|
||||
border-top-color: var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
.add-span-to-funnel-modal__discard-button {
|
||||
background: var(--bg-vanilla-200);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@ function FunnelDetailsView({
|
||||
funnel={funnel}
|
||||
isTraceDetailsPage
|
||||
span={span}
|
||||
disableAutoSave
|
||||
triggerAutoSave={triggerAutoSave}
|
||||
showNotifications={showNotifications}
|
||||
/>
|
||||
@@ -143,13 +142,19 @@ function AddSpanToFunnelModal({
|
||||
const handleSaveFunnel = (): void => {
|
||||
setTriggerSave(true);
|
||||
// Reset trigger after a brief moment to allow the save to be processed
|
||||
setTimeout(() => setTriggerSave(false), 100);
|
||||
setTimeout(() => {
|
||||
setTriggerSave(false);
|
||||
onClose();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleDiscard = (): void => {
|
||||
setTriggerDiscard(true);
|
||||
// Reset trigger after a brief moment
|
||||
setTimeout(() => setTriggerDiscard(false), 100);
|
||||
setTimeout(() => {
|
||||
setTriggerDiscard(false);
|
||||
onClose();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const renderListView = (): JSX.Element => (
|
||||
@@ -239,9 +244,6 @@ function AddSpanToFunnelModal({
|
||||
footer={
|
||||
activeView === ModalView.DETAILS
|
||||
? [
|
||||
<Button key="close" onClick={onClose}>
|
||||
Close
|
||||
</Button>,
|
||||
<Button
|
||||
type="default"
|
||||
key="discard"
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { FunnelData, FunnelStepData } from 'types/api/traceFunnels';
|
||||
|
||||
import { useUpdateFunnelSteps } from './useFunnels';
|
||||
@@ -13,22 +16,30 @@ interface UseFunnelConfiguration {
|
||||
isPopoverOpen: boolean;
|
||||
setIsPopoverOpen: (isPopoverOpen: boolean) => void;
|
||||
steps: FunnelStepData[];
|
||||
isSaving: boolean;
|
||||
}
|
||||
|
||||
// Add this helper function
|
||||
const normalizeSteps = (steps: FunnelStepData[]): FunnelStepData[] => {
|
||||
export const normalizeSteps = (steps: FunnelStepData[]): FunnelStepData[] => {
|
||||
if (steps.some((step) => !step.filters)) return steps;
|
||||
|
||||
return steps.map((step) => ({
|
||||
...step,
|
||||
filters: {
|
||||
...step.filters,
|
||||
items: step.filters.items.map((item) => ({
|
||||
id: '',
|
||||
key: item.key,
|
||||
value: item.value,
|
||||
op: item.op,
|
||||
})),
|
||||
items: step.filters.items.map((item) => {
|
||||
const {
|
||||
id: unusedId,
|
||||
isIndexed,
|
||||
...keyObj
|
||||
} = item.key as BaseAutocompleteData;
|
||||
return {
|
||||
id: '',
|
||||
key: keyObj,
|
||||
value: item.value,
|
||||
op: item.op,
|
||||
};
|
||||
}),
|
||||
},
|
||||
}));
|
||||
};
|
||||
@@ -36,22 +47,22 @@ const normalizeSteps = (steps: FunnelStepData[]): FunnelStepData[] => {
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function useFunnelConfiguration({
|
||||
funnel,
|
||||
disableAutoSave = false,
|
||||
triggerAutoSave = false,
|
||||
showNotifications = false,
|
||||
}: {
|
||||
funnel: FunnelData;
|
||||
disableAutoSave?: boolean;
|
||||
triggerAutoSave?: boolean;
|
||||
showNotifications?: boolean;
|
||||
}): UseFunnelConfiguration {
|
||||
const { notifications } = useNotifications();
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
steps,
|
||||
initialSteps,
|
||||
hasIncompleteStepFields,
|
||||
lastUpdatedSteps,
|
||||
setLastUpdatedSteps,
|
||||
handleRestoreSteps,
|
||||
handleRunFunnel,
|
||||
selectedTime,
|
||||
setIsUpdatingFunnel,
|
||||
} = useFunnelContext();
|
||||
|
||||
// State management
|
||||
@@ -59,10 +70,6 @@ export default function useFunnelConfiguration({
|
||||
|
||||
const debouncedSteps = useDebounce(steps, 200);
|
||||
|
||||
const [lastValidatedSteps, setLastValidatedSteps] = useState<FunnelStepData[]>(
|
||||
initialSteps,
|
||||
);
|
||||
|
||||
// Mutation hooks
|
||||
const updateStepsMutation = useUpdateFunnelSteps(
|
||||
funnel.funnel_id,
|
||||
@@ -71,6 +78,15 @@ export default function useFunnelConfiguration({
|
||||
|
||||
// Derived state
|
||||
const lastSavedStepsStateRef = useRef<FunnelStepData[]>(steps);
|
||||
const hasRestoredFromLocalStorage = useRef(false);
|
||||
|
||||
// localStorage hook for funnel steps
|
||||
const localStorageKey = `${LOCALSTORAGE.FUNNEL_STEPS}_${funnel.funnel_id}`;
|
||||
const [
|
||||
localStorageSavedSteps,
|
||||
setLocalStorageSavedSteps,
|
||||
clearLocalStorageSavedSteps,
|
||||
] = useLocalStorage<FunnelStepData[] | null>(localStorageKey, null);
|
||||
|
||||
const hasStepsChanged = useCallback(() => {
|
||||
const normalizedLastSavedSteps = normalizeSteps(
|
||||
@@ -80,6 +96,34 @@ export default function useFunnelConfiguration({
|
||||
return !isEqual(normalizedDebouncedSteps, normalizedLastSavedSteps);
|
||||
}, [debouncedSteps]);
|
||||
|
||||
// Handle localStorage for funnel steps
|
||||
useEffect(() => {
|
||||
// Restore from localStorage on first run if
|
||||
if (!hasRestoredFromLocalStorage.current) {
|
||||
const savedSteps = localStorageSavedSteps;
|
||||
if (savedSteps) {
|
||||
handleRestoreSteps(savedSteps);
|
||||
hasRestoredFromLocalStorage.current = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Save steps to localStorage
|
||||
if (hasStepsChanged()) {
|
||||
setLocalStorageSavedSteps(debouncedSteps);
|
||||
}
|
||||
}, [
|
||||
debouncedSteps,
|
||||
funnel.funnel_id,
|
||||
hasStepsChanged,
|
||||
handleRestoreSteps,
|
||||
localStorageSavedSteps,
|
||||
setLocalStorageSavedSteps,
|
||||
queryClient,
|
||||
selectedTime,
|
||||
lastUpdatedSteps,
|
||||
]);
|
||||
|
||||
const hasFunnelStepDefinitionsChanged = useCallback(
|
||||
(prevSteps: FunnelStepData[], nextSteps: FunnelStepData[]): boolean => {
|
||||
if (prevSteps.length !== nextSteps.length) return true;
|
||||
@@ -97,15 +141,6 @@ export default function useFunnelConfiguration({
|
||||
[],
|
||||
);
|
||||
|
||||
const hasFunnelLatencyTypeChanged = useCallback(
|
||||
(prevSteps: FunnelStepData[], nextSteps: FunnelStepData[]): boolean =>
|
||||
prevSteps.some((step, index) => {
|
||||
const nextStep = nextSteps[index];
|
||||
return step.latency_type !== nextStep.latency_type;
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
// Mutation payload preparation
|
||||
const getUpdatePayload = useCallback(
|
||||
() => ({
|
||||
@@ -116,33 +151,19 @@ export default function useFunnelConfiguration({
|
||||
[funnel.funnel_id, debouncedSteps],
|
||||
);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { selectedTime } = useFunnelContext();
|
||||
|
||||
const validateStepsQueryKey = useMemo(
|
||||
() => [REACT_QUERY_KEY.VALIDATE_FUNNEL_STEPS, funnel.funnel_id, selectedTime],
|
||||
[funnel.funnel_id, selectedTime],
|
||||
);
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
useEffect(() => {
|
||||
// Determine if we should save based on the mode
|
||||
let shouldSave = false;
|
||||
|
||||
if (disableAutoSave) {
|
||||
// Manual save mode: only save when explicitly triggered
|
||||
shouldSave = triggerAutoSave;
|
||||
} else {
|
||||
// Auto-save mode: save when steps have changed and no incomplete fields
|
||||
shouldSave = hasStepsChanged() && !hasIncompleteStepFields;
|
||||
}
|
||||
|
||||
if (shouldSave && !isEqual(debouncedSteps, lastValidatedSteps)) {
|
||||
if (triggerAutoSave && !isEqual(debouncedSteps, lastUpdatedSteps)) {
|
||||
setIsUpdatingFunnel(true);
|
||||
updateStepsMutation.mutate(getUpdatePayload(), {
|
||||
onSuccess: (data) => {
|
||||
const updatedFunnelSteps = data?.payload?.steps;
|
||||
|
||||
if (!updatedFunnelSteps) return;
|
||||
|
||||
// Clear localStorage since steps are saved successfully
|
||||
clearLocalStorageSavedSteps();
|
||||
|
||||
queryClient.setQueryData(
|
||||
[REACT_QUERY_KEY.GET_FUNNEL_DETAILS, funnel.funnel_id],
|
||||
(oldData: any) => {
|
||||
@@ -163,17 +184,9 @@ export default function useFunnelConfiguration({
|
||||
(step) => step.service_name === '' || step.span_name === '',
|
||||
);
|
||||
|
||||
if (hasFunnelLatencyTypeChanged(lastValidatedSteps, debouncedSteps)) {
|
||||
handleRunFunnel();
|
||||
setLastValidatedSteps(debouncedSteps);
|
||||
}
|
||||
// Only validate if funnel steps definitions
|
||||
else if (
|
||||
!hasIncompleteStepFields &&
|
||||
hasFunnelStepDefinitionsChanged(lastValidatedSteps, debouncedSteps)
|
||||
) {
|
||||
queryClient.refetchQueries(validateStepsQueryKey);
|
||||
setLastValidatedSteps(debouncedSteps);
|
||||
if (!hasIncompleteStepFields) {
|
||||
setLastUpdatedSteps(debouncedSteps);
|
||||
}
|
||||
|
||||
// Show success notification only when requested
|
||||
@@ -216,17 +229,18 @@ export default function useFunnelConfiguration({
|
||||
getUpdatePayload,
|
||||
hasFunnelStepDefinitionsChanged,
|
||||
hasStepsChanged,
|
||||
lastValidatedSteps,
|
||||
lastUpdatedSteps,
|
||||
queryClient,
|
||||
validateStepsQueryKey,
|
||||
triggerAutoSave,
|
||||
showNotifications,
|
||||
disableAutoSave,
|
||||
localStorageSavedSteps,
|
||||
clearLocalStorageSavedSteps,
|
||||
]);
|
||||
|
||||
return {
|
||||
isPopoverOpen,
|
||||
setIsPopoverOpen,
|
||||
steps,
|
||||
isSaving: updateStepsMutation.isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,10 +20,11 @@ export function useFunnelMetrics({
|
||||
metricsData: MetricItem[];
|
||||
conversionRate: number;
|
||||
} {
|
||||
const { startTime, endTime } = useFunnelContext();
|
||||
const { startTime, endTime, steps } = useFunnelContext();
|
||||
const payload = {
|
||||
start_time: startTime,
|
||||
end_time: endTime,
|
||||
steps,
|
||||
};
|
||||
|
||||
const {
|
||||
@@ -81,6 +82,7 @@ export function useFunnelStepsMetrics({
|
||||
end_time: endTime,
|
||||
step_start: stepStart,
|
||||
step_end: stepEnd,
|
||||
steps,
|
||||
};
|
||||
|
||||
const {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
FunnelOverviewResponse,
|
||||
FunnelStepsOverviewPayload,
|
||||
FunnelStepsOverviewResponse,
|
||||
FunnelStepsPayload,
|
||||
FunnelStepsResponse,
|
||||
getFunnelById,
|
||||
getFunnelErrorTraces,
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
CreateFunnelPayload,
|
||||
CreateFunnelResponse,
|
||||
FunnelData,
|
||||
FunnelStepData,
|
||||
} from 'types/api/traceFunnels';
|
||||
|
||||
export const useFunnelsList = (): UseQueryResult<
|
||||
@@ -117,12 +119,14 @@ export const useValidateFunnelSteps = ({
|
||||
startTime,
|
||||
endTime,
|
||||
enabled,
|
||||
steps,
|
||||
}: {
|
||||
funnelId: string;
|
||||
selectedTime: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
enabled: boolean;
|
||||
steps: FunnelStepData[];
|
||||
}): UseQueryResult<
|
||||
SuccessResponse<ValidateFunnelResponse> | ErrorResponse,
|
||||
Error
|
||||
@@ -130,11 +134,19 @@ export const useValidateFunnelSteps = ({
|
||||
useQuery({
|
||||
queryFn: ({ signal }) =>
|
||||
validateFunnelSteps(
|
||||
funnelId,
|
||||
{ start_time: startTime, end_time: endTime },
|
||||
{ start_time: startTime, end_time: endTime, steps },
|
||||
signal,
|
||||
),
|
||||
queryKey: [REACT_QUERY_KEY.VALIDATE_FUNNEL_STEPS, funnelId, selectedTime],
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.VALIDATE_FUNNEL_STEPS,
|
||||
funnelId,
|
||||
selectedTime,
|
||||
steps.map((step) => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { latency_type, ...rest } = step;
|
||||
return rest;
|
||||
}),
|
||||
],
|
||||
enabled,
|
||||
staleTime: 0,
|
||||
});
|
||||
@@ -168,18 +180,17 @@ export const useFunnelOverview = (
|
||||
const {
|
||||
selectedTime,
|
||||
validTracesCount,
|
||||
hasFunnelBeenExecuted,
|
||||
isUpdatingFunnel,
|
||||
} = useFunnelContext();
|
||||
return useQuery({
|
||||
queryFn: ({ signal }) => getFunnelOverview(funnelId, payload, signal),
|
||||
queryFn: ({ signal }) => getFunnelOverview(payload, signal),
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_FUNNEL_OVERVIEW,
|
||||
funnelId,
|
||||
selectedTime,
|
||||
payload.step_start ?? '',
|
||||
payload.step_end ?? '',
|
||||
payload.steps,
|
||||
],
|
||||
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted,
|
||||
enabled: !!funnelId && validTracesCount > 0 && !isUpdatingFunnel,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -190,18 +201,19 @@ export const useFunnelSlowTraces = (
|
||||
const {
|
||||
selectedTime,
|
||||
validTracesCount,
|
||||
hasFunnelBeenExecuted,
|
||||
isUpdatingFunnel,
|
||||
} = useFunnelContext();
|
||||
return useQuery<SuccessResponse<SlowTraceData> | ErrorResponse, Error>({
|
||||
queryFn: ({ signal }) => getFunnelSlowTraces(funnelId, payload, signal),
|
||||
queryFn: ({ signal }) => getFunnelSlowTraces(payload, signal),
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_FUNNEL_SLOW_TRACES,
|
||||
funnelId,
|
||||
selectedTime,
|
||||
payload.step_start ?? '',
|
||||
payload.step_end ?? '',
|
||||
payload.steps,
|
||||
],
|
||||
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted,
|
||||
enabled: !!funnelId && validTracesCount > 0 && !isUpdatingFunnel,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -212,7 +224,7 @@ export const useFunnelErrorTraces = (
|
||||
const {
|
||||
selectedTime,
|
||||
validTracesCount,
|
||||
hasFunnelBeenExecuted,
|
||||
isUpdatingFunnel,
|
||||
} = useFunnelContext();
|
||||
return useQuery({
|
||||
queryFn: ({ signal }) => getFunnelErrorTraces(funnelId, payload, signal),
|
||||
@@ -222,35 +234,31 @@ export const useFunnelErrorTraces = (
|
||||
selectedTime,
|
||||
payload.step_start ?? '',
|
||||
payload.step_end ?? '',
|
||||
payload.steps,
|
||||
],
|
||||
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted,
|
||||
enabled: !!funnelId && validTracesCount > 0 && !isUpdatingFunnel,
|
||||
});
|
||||
};
|
||||
|
||||
export function useFunnelStepsGraphData(
|
||||
funnelId: string,
|
||||
payload: FunnelStepsPayload,
|
||||
): UseQueryResult<SuccessResponse<FunnelStepsResponse> | ErrorResponse, Error> {
|
||||
const {
|
||||
startTime,
|
||||
endTime,
|
||||
selectedTime,
|
||||
validTracesCount,
|
||||
hasFunnelBeenExecuted,
|
||||
isUpdatingFunnel,
|
||||
} = useFunnelContext();
|
||||
|
||||
return useQuery({
|
||||
queryFn: ({ signal }) =>
|
||||
getFunnelSteps(
|
||||
funnelId,
|
||||
{ start_time: startTime, end_time: endTime },
|
||||
signal,
|
||||
),
|
||||
queryFn: ({ signal }) => getFunnelSteps(payload, signal),
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_FUNNEL_STEPS_GRAPH_DATA,
|
||||
funnelId,
|
||||
selectedTime,
|
||||
payload.steps,
|
||||
],
|
||||
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted,
|
||||
enabled: !!funnelId && validTracesCount > 0 && !isUpdatingFunnel,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -264,17 +272,18 @@ export const useFunnelStepsOverview = (
|
||||
const {
|
||||
selectedTime,
|
||||
validTracesCount,
|
||||
hasFunnelBeenExecuted,
|
||||
isUpdatingFunnel,
|
||||
} = useFunnelContext();
|
||||
return useQuery({
|
||||
queryFn: ({ signal }) => getFunnelStepsOverview(funnelId, payload, signal),
|
||||
queryFn: ({ signal }) => getFunnelStepsOverview(payload, signal),
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_FUNNEL_STEPS_OVERVIEW,
|
||||
funnelId,
|
||||
selectedTime,
|
||||
payload.step_start ?? '',
|
||||
payload.step_end ?? '',
|
||||
payload.steps,
|
||||
],
|
||||
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted,
|
||||
enabled: !!funnelId && validTracesCount > 0 && !isUpdatingFunnel,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import NewWidget from 'container/NewWidget';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { generatePath, useLocation, useParams } from 'react-router-dom';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
@@ -52,11 +53,13 @@ function DashboardWidget(): JSX.Element | null {
|
||||
}
|
||||
|
||||
return (
|
||||
<NewWidget
|
||||
yAxisUnit={selectedWidget?.yAxisUnit}
|
||||
selectedGraph={selectedGraph}
|
||||
fillSpans={selectedWidget?.fillSpans}
|
||||
/>
|
||||
<PreferenceContextProvider>
|
||||
<NewWidget
|
||||
yAxisUnit={selectedWidget?.yAxisUnit}
|
||||
selectedGraph={selectedGraph}
|
||||
fillSpans={selectedWidget?.fillSpans}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,14 @@ import ROUTES from 'constants/routes';
|
||||
import InfraMonitoringHosts from 'container/InfraMonitoringHosts';
|
||||
import InfraMonitoringK8s from 'container/InfraMonitoringK8s';
|
||||
import { Inbox } from 'lucide-react';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
|
||||
export const Hosts: TabRoutes = {
|
||||
Component: InfraMonitoringHosts,
|
||||
Component: (): JSX.Element => (
|
||||
<PreferenceContextProvider>
|
||||
<InfraMonitoringHosts />
|
||||
</PreferenceContextProvider>
|
||||
),
|
||||
name: (
|
||||
<div className="tab-item">
|
||||
<Inbox size={16} /> Hosts
|
||||
@@ -16,7 +21,11 @@ export const Hosts: TabRoutes = {
|
||||
};
|
||||
|
||||
export const Kubernetes: TabRoutes = {
|
||||
Component: InfraMonitoringK8s,
|
||||
Component: (): JSX.Element => (
|
||||
<PreferenceContextProvider>
|
||||
<InfraMonitoringK8s />
|
||||
</PreferenceContextProvider>
|
||||
),
|
||||
name: (
|
||||
<div className="tab-item">
|
||||
<Inbox size={16} /> Kubernetes
|
||||
|
||||
@@ -10,6 +10,7 @@ import LogsFilters from 'container/LogsFilters';
|
||||
import LogsSearchFilter from 'container/LogsSearchFilter';
|
||||
import LogsTable from 'container/LogsTable';
|
||||
import history from 'lib/history';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@@ -82,69 +83,71 @@ function OldLogsExplorer(): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="old-logs-explorer">
|
||||
<SpaceContainer
|
||||
split={<Divider type="vertical" />}
|
||||
align="center"
|
||||
direction="horizontal"
|
||||
>
|
||||
<LogsSearchFilter />
|
||||
<LogLiveTail />
|
||||
</SpaceContainer>
|
||||
<PreferenceContextProvider>
|
||||
<div className="old-logs-explorer">
|
||||
<SpaceContainer
|
||||
split={<Divider type="vertical" />}
|
||||
align="center"
|
||||
direction="horizontal"
|
||||
>
|
||||
<LogsSearchFilter />
|
||||
<LogLiveTail />
|
||||
</SpaceContainer>
|
||||
|
||||
<LogsAggregate />
|
||||
<LogsAggregate />
|
||||
|
||||
<Row gutter={20} wrap={false}>
|
||||
<LogsFilters />
|
||||
<Col flex={1} className="logs-col-container">
|
||||
<Row>
|
||||
<Col flex={1}>
|
||||
<Space align="baseline" direction="horizontal">
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
style={defaultSelectStyle}
|
||||
value={selectedViewModeOption}
|
||||
onChange={onChangeVeiwMode}
|
||||
>
|
||||
{viewModeOptionList.map((option) => (
|
||||
<Select.Option key={option.value}>{option.label}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{isFormatButtonVisible && (
|
||||
<Popover
|
||||
<Row gutter={20} wrap={false}>
|
||||
<LogsFilters />
|
||||
<Col flex={1} className="logs-col-container">
|
||||
<Row>
|
||||
<Col flex={1}>
|
||||
<Space align="baseline" direction="horizontal">
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
placement="right"
|
||||
content={renderPopoverContent}
|
||||
style={defaultSelectStyle}
|
||||
value={selectedViewModeOption}
|
||||
onChange={onChangeVeiwMode}
|
||||
>
|
||||
<Button>Format</Button>
|
||||
</Popover>
|
||||
)}
|
||||
{viewModeOptionList.map((option) => (
|
||||
<Select.Option key={option.value}>{option.label}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
style={defaultSelectStyle}
|
||||
defaultValue={order}
|
||||
onChange={handleChangeOrder}
|
||||
>
|
||||
{orderItems.map((item) => (
|
||||
<Select.Option key={item.enum}>{item.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Space>
|
||||
</Col>
|
||||
{isFormatButtonVisible && (
|
||||
<Popover
|
||||
getPopupContainer={popupContainer}
|
||||
placement="right"
|
||||
content={renderPopoverContent}
|
||||
>
|
||||
<Button>Format</Button>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
<Col>
|
||||
<LogControls />
|
||||
</Col>
|
||||
</Row>
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
style={defaultSelectStyle}
|
||||
defaultValue={order}
|
||||
onChange={handleChangeOrder}
|
||||
>
|
||||
{orderItems.map((item) => (
|
||||
<Select.Option key={item.enum}>{item.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<LogsTable viewMode={viewMode} linesPerRow={linesPerRow} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Col>
|
||||
<LogControls />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<LogDetailedView />
|
||||
</div>
|
||||
<LogsTable viewMode={viewMode} linesPerRow={linesPerRow} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<LogDetailedView />
|
||||
</div>
|
||||
</PreferenceContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import NotFound from 'components/NotFound';
|
||||
import Spinner from 'components/Spinner';
|
||||
import NewDashboard from 'container/NewDashboard';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { useEffect } from 'react';
|
||||
import { ErrorType } from 'types/common';
|
||||
|
||||
@@ -35,7 +36,11 @@ function DashboardPage(): JSX.Element {
|
||||
return <Spinner tip="Loading.." />;
|
||||
}
|
||||
|
||||
return <NewDashboard />;
|
||||
return (
|
||||
<PreferenceContextProvider>
|
||||
<NewDashboard />
|
||||
</PreferenceContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardPage;
|
||||
|
||||
@@ -2,6 +2,7 @@ import './DeleteFunnelStep.styles.scss';
|
||||
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import { Trash2, X } from 'lucide-react';
|
||||
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
||||
|
||||
interface DeleteFunnelStepProps {
|
||||
isOpen: boolean;
|
||||
@@ -14,8 +15,10 @@ function DeleteFunnelStep({
|
||||
onClose,
|
||||
onStepRemove,
|
||||
}: DeleteFunnelStepProps): JSX.Element {
|
||||
const { handleRunFunnel } = useFunnelContext();
|
||||
const handleStepRemoval = (): void => {
|
||||
onStepRemove();
|
||||
handleRunFunnel();
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import useFunnelConfiguration from 'hooks/TracesFunnels/useFunnelConfiguration';
|
||||
import { PencilLine } from 'lucide-react';
|
||||
import FunnelItemPopover from 'pages/TracesFunnels/components/FunnelsList/FunnelItemPopover';
|
||||
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
||||
import CopyToClipboard from 'periscope/components/CopyToClipboard';
|
||||
import { memo, useState } from 'react';
|
||||
import { Span } from 'types/api/trace/getTraceV2';
|
||||
@@ -21,7 +22,6 @@ interface FunnelConfigurationProps {
|
||||
funnel: FunnelData;
|
||||
isTraceDetailsPage?: boolean;
|
||||
span?: Span;
|
||||
disableAutoSave?: boolean;
|
||||
triggerAutoSave?: boolean;
|
||||
showNotifications?: boolean;
|
||||
}
|
||||
@@ -30,15 +30,19 @@ function FunnelConfiguration({
|
||||
funnel,
|
||||
isTraceDetailsPage,
|
||||
span,
|
||||
disableAutoSave,
|
||||
triggerAutoSave,
|
||||
showNotifications,
|
||||
}: FunnelConfigurationProps): JSX.Element {
|
||||
const { isPopoverOpen, setIsPopoverOpen, steps } = useFunnelConfiguration({
|
||||
const { triggerSave } = useFunnelContext();
|
||||
const {
|
||||
isPopoverOpen,
|
||||
setIsPopoverOpen,
|
||||
steps,
|
||||
isSaving,
|
||||
} = useFunnelConfiguration({
|
||||
funnel,
|
||||
disableAutoSave,
|
||||
triggerAutoSave,
|
||||
showNotifications,
|
||||
triggerAutoSave: triggerAutoSave || triggerSave,
|
||||
showNotifications: showNotifications || triggerSave,
|
||||
});
|
||||
const [isDescriptionModalOpen, setIsDescriptionModalOpen] = useState<boolean>(
|
||||
false,
|
||||
@@ -106,7 +110,7 @@ function FunnelConfiguration({
|
||||
|
||||
{!isTraceDetailsPage && (
|
||||
<>
|
||||
<StepsFooter stepsCount={steps.length} />
|
||||
<StepsFooter stepsCount={steps.length} isSaving={isSaving || false} />
|
||||
<AddFunnelDescriptionModal
|
||||
isOpen={isDescriptionModalOpen}
|
||||
onClose={handleDescriptionModalClose}
|
||||
@@ -122,7 +126,6 @@ function FunnelConfiguration({
|
||||
FunnelConfiguration.defaultProps = {
|
||||
isTraceDetailsPage: false,
|
||||
span: undefined,
|
||||
disableAutoSave: false,
|
||||
triggerAutoSave: false,
|
||||
showNotifications: false,
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
color: var(--bg-vanilla-400);
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
border-radius: 6px;
|
||||
width: 100%;
|
||||
.step-popover {
|
||||
opacity: 0;
|
||||
width: 22px;
|
||||
|
||||
@@ -40,11 +40,6 @@
|
||||
letter-spacing: 0.12px;
|
||||
border-radius: 2px;
|
||||
|
||||
&--sync {
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
&--run {
|
||||
background-color: var(--bg-robin-500);
|
||||
}
|
||||
|
||||
@@ -1,53 +1,14 @@
|
||||
import './StepsFooter.styles.scss';
|
||||
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Button, Skeleton, Spin } from 'antd';
|
||||
import { Button, Skeleton } from 'antd';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { Cone, Play, RefreshCcw } from 'lucide-react';
|
||||
import { Check, Cone } from 'lucide-react';
|
||||
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
||||
import { useMemo } from 'react';
|
||||
import { useIsFetching, useIsMutating } from 'react-query';
|
||||
|
||||
const useFunnelResultsLoading = (): boolean => {
|
||||
const { funnelId } = useFunnelContext();
|
||||
|
||||
const isFetchingFunnelOverview = useIsFetching({
|
||||
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_OVERVIEW, funnelId],
|
||||
});
|
||||
|
||||
const isFetchingStepsGraphData = useIsFetching({
|
||||
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_STEPS_GRAPH_DATA, funnelId],
|
||||
});
|
||||
|
||||
const isFetchingErrorTraces = useIsFetching({
|
||||
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_ERROR_TRACES, funnelId],
|
||||
});
|
||||
|
||||
const isFetchingSlowTraces = useIsFetching({
|
||||
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_SLOW_TRACES, funnelId],
|
||||
});
|
||||
|
||||
return useMemo(() => {
|
||||
if (!funnelId) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
!!isFetchingFunnelOverview ||
|
||||
!!isFetchingStepsGraphData ||
|
||||
!!isFetchingErrorTraces ||
|
||||
!!isFetchingSlowTraces
|
||||
);
|
||||
}, [
|
||||
funnelId,
|
||||
isFetchingFunnelOverview,
|
||||
isFetchingStepsGraphData,
|
||||
isFetchingErrorTraces,
|
||||
isFetchingSlowTraces,
|
||||
]);
|
||||
};
|
||||
import { useIsMutating } from 'react-query';
|
||||
|
||||
interface StepsFooterProps {
|
||||
stepsCount: number;
|
||||
isSaving: boolean;
|
||||
}
|
||||
|
||||
function ValidTracesCount(): JSX.Element {
|
||||
@@ -93,21 +54,13 @@ function ValidTracesCount(): JSX.Element {
|
||||
return <span className="steps-footer__valid-traces">Valid traces found</span>;
|
||||
}
|
||||
|
||||
function StepsFooter({ stepsCount }: StepsFooterProps): JSX.Element {
|
||||
function StepsFooter({ stepsCount, isSaving }: StepsFooterProps): JSX.Element {
|
||||
const {
|
||||
validTracesCount,
|
||||
handleRunFunnel,
|
||||
hasFunnelBeenExecuted,
|
||||
funnelId,
|
||||
hasIncompleteStepFields,
|
||||
handleSaveFunnel,
|
||||
hasUnsavedChanges,
|
||||
} = useFunnelContext();
|
||||
|
||||
const isFunnelResultsLoading = useFunnelResultsLoading();
|
||||
|
||||
const isFunnelUpdateMutating = useIsMutating([
|
||||
REACT_QUERY_KEY.UPDATE_FUNNEL_STEPS,
|
||||
funnelId,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="steps-footer">
|
||||
<div className="steps-footer__left">
|
||||
@@ -117,38 +70,16 @@ function StepsFooter({ stepsCount }: StepsFooterProps): JSX.Element {
|
||||
<ValidTracesCount />
|
||||
</div>
|
||||
<div className="steps-footer__right">
|
||||
{!!isFunnelUpdateMutating && (
|
||||
<div className="steps-footer__button steps-footer__button--updating">
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ color: 'grey' }} />}
|
||||
size="small"
|
||||
/>
|
||||
Updating
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasFunnelBeenExecuted ? (
|
||||
<Button
|
||||
disabled={validTracesCount === 0}
|
||||
onClick={handleRunFunnel}
|
||||
type="primary"
|
||||
className="steps-footer__button steps-footer__button--run"
|
||||
icon={<Play size={16} />}
|
||||
>
|
||||
Run funnel
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="text"
|
||||
className="steps-footer__button steps-footer__button--sync"
|
||||
icon={<RefreshCcw size={16} />}
|
||||
onClick={handleRunFunnel}
|
||||
loading={isFunnelResultsLoading}
|
||||
disabled={validTracesCount === 0}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
disabled={hasIncompleteStepFields || !hasUnsavedChanges}
|
||||
onClick={handleSaveFunnel}
|
||||
type="primary"
|
||||
className="steps-footer__button steps-footer__button--run"
|
||||
icon={<Check size={14} />}
|
||||
loading={isSaving}
|
||||
>
|
||||
Save funnel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -29,13 +29,20 @@ Chart.register(
|
||||
);
|
||||
|
||||
function FunnelGraph(): JSX.Element {
|
||||
const { funnelId } = useFunnelContext();
|
||||
const { funnelId, startTime, endTime, steps } = useFunnelContext();
|
||||
|
||||
const payload = {
|
||||
start_time: startTime,
|
||||
end_time: endTime,
|
||||
steps,
|
||||
};
|
||||
|
||||
const {
|
||||
data: stepsData,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isError,
|
||||
} = useFunnelStepsGraphData(funnelId);
|
||||
} = useFunnelStepsGraphData(funnelId, payload);
|
||||
|
||||
const data = useMemo(() => stepsData?.payload?.data?.[0]?.data, [
|
||||
stepsData?.payload?.data,
|
||||
|
||||
@@ -16,7 +16,6 @@ function FunnelResults(): JSX.Element {
|
||||
isValidateStepsLoading,
|
||||
hasIncompleteStepFields,
|
||||
hasAllEmptyStepFields,
|
||||
hasFunnelBeenExecuted,
|
||||
funnelId,
|
||||
} = useFunnelContext();
|
||||
|
||||
@@ -47,14 +46,6 @@ function FunnelResults(): JSX.Element {
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!hasFunnelBeenExecuted) {
|
||||
return (
|
||||
<EmptyFunnelResults
|
||||
title="Funnel has not been run yet."
|
||||
description="Run the funnel to see the results"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="funnel-results">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
||||
import { useMemo } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { FunnelStepData } from 'types/api/traceFunnels';
|
||||
|
||||
import FunnelTable from './FunnelTable';
|
||||
import { topTracesTableColumns } from './utils';
|
||||
@@ -24,6 +25,7 @@ interface FunnelTopTracesTableProps {
|
||||
SuccessResponse<SlowTraceData | ErrorTraceData> | ErrorResponse,
|
||||
Error
|
||||
>;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
function FunnelTopTracesTable({
|
||||
@@ -32,6 +34,7 @@ function FunnelTopTracesTable({
|
||||
stepBOrder,
|
||||
title,
|
||||
tooltip,
|
||||
steps,
|
||||
useQueryHook,
|
||||
}: FunnelTopTracesTableProps): JSX.Element {
|
||||
const { startTime, endTime } = useFunnelContext();
|
||||
@@ -41,8 +44,9 @@ function FunnelTopTracesTable({
|
||||
end_time: endTime,
|
||||
step_start: stepAOrder,
|
||||
step_end: stepBOrder,
|
||||
steps,
|
||||
}),
|
||||
[startTime, endTime, stepAOrder, stepBOrder],
|
||||
[startTime, endTime, stepAOrder, stepBOrder, steps],
|
||||
);
|
||||
|
||||
const { data: response, isLoading, isFetching } = useQueryHook(
|
||||
|
||||
@@ -6,7 +6,7 @@ import FunnelMetricsTable from './FunnelMetricsTable';
|
||||
function OverallMetrics(): JSX.Element {
|
||||
const { funnelId } = useParams<{ funnelId: string }>();
|
||||
const { isLoading, metricsData, conversionRate, isError } = useFunnelMetrics({
|
||||
funnelId: funnelId || '',
|
||||
funnelId,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -52,11 +52,13 @@ function StepsTransitionResults(): JSX.Element {
|
||||
funnelId={funnelId}
|
||||
stepAOrder={stepAOrder}
|
||||
stepBOrder={stepBOrder}
|
||||
steps={steps}
|
||||
/>
|
||||
<TopTracesWithErrors
|
||||
funnelId={funnelId}
|
||||
stepAOrder={stepAOrder}
|
||||
stepBOrder={stepBOrder}
|
||||
steps={steps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useFunnelSlowTraces } from 'hooks/TracesFunnels/useFunnels';
|
||||
import { FunnelStepData } from 'types/api/traceFunnels';
|
||||
|
||||
import FunnelTopTracesTable from './FunnelTopTracesTable';
|
||||
|
||||
@@ -6,6 +7,7 @@ interface TopSlowestTracesProps {
|
||||
funnelId: string;
|
||||
stepAOrder: number;
|
||||
stepBOrder: number;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
function TopSlowestTraces(props: TopSlowestTracesProps): JSX.Element {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useFunnelErrorTraces } from 'hooks/TracesFunnels/useFunnels';
|
||||
import { FunnelStepData } from 'types/api/traceFunnels';
|
||||
|
||||
import FunnelTopTracesTable from './FunnelTopTracesTable';
|
||||
|
||||
@@ -6,6 +7,7 @@ interface TopTracesWithErrorsProps {
|
||||
funnelId: string;
|
||||
stepAOrder: number;
|
||||
stepBOrder: number;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
function TopTracesWithErrors(props: TopTracesWithErrorsProps): JSX.Element {
|
||||
|
||||
@@ -18,10 +18,4 @@ export const topTracesTableColumns = [
|
||||
key: 'duration_ms',
|
||||
render: (value: string): string => getYAxisFormattedValue(value, 'ms'),
|
||||
},
|
||||
{
|
||||
title: 'SPAN COUNT',
|
||||
dataIndex: 'span_count',
|
||||
key: 'span_count',
|
||||
render: (value: number): string => value.toString(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -14,8 +14,6 @@ export const initialStepsData: FunnelStepData[] = [
|
||||
latency_pointer: 'start',
|
||||
latency_type: undefined,
|
||||
has_errors: false,
|
||||
name: '',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
@@ -29,8 +27,6 @@ export const initialStepsData: FunnelStepData[] = [
|
||||
latency_pointer: 'start',
|
||||
latency_type: LatencyOptions.P95,
|
||||
has_errors: false,
|
||||
name: '',
|
||||
description: '',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ValidateFunnelResponse } from 'api/traceFunnels';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time as TimeV2,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { normalizeSteps } from 'hooks/TracesFunnels/useFunnelConfiguration';
|
||||
import { useValidateFunnelSteps } from 'hooks/TracesFunnels/useFunnels';
|
||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { initialStepsData } from 'pages/TracesFunnelDetails/constants';
|
||||
import {
|
||||
createContext,
|
||||
@@ -41,6 +41,9 @@ interface FunnelContextType {
|
||||
handleStepChange: (index: number, newStep: Partial<FunnelStepData>) => void;
|
||||
handleStepRemoval: (index: number) => void;
|
||||
handleRunFunnel: () => void;
|
||||
handleSaveFunnel: () => void;
|
||||
triggerSave: boolean;
|
||||
hasUnsavedChanges: boolean;
|
||||
validationResponse:
|
||||
| SuccessResponse<ValidateFunnelResponse>
|
||||
| ErrorResponse
|
||||
@@ -54,8 +57,10 @@ interface FunnelContextType {
|
||||
spanName: string,
|
||||
) => void;
|
||||
handleRestoreSteps: (oldSteps: FunnelStepData[]) => void;
|
||||
hasFunnelBeenExecuted: boolean;
|
||||
setHasFunnelBeenExecuted: Dispatch<SetStateAction<boolean>>;
|
||||
isUpdatingFunnel: boolean;
|
||||
setIsUpdatingFunnel: Dispatch<SetStateAction<boolean>>;
|
||||
lastUpdatedSteps: FunnelStepData[];
|
||||
setLastUpdatedSteps: Dispatch<SetStateAction<FunnelStepData[]>>;
|
||||
}
|
||||
|
||||
const FunnelContext = createContext<FunnelContextType | undefined>(undefined);
|
||||
@@ -86,6 +91,19 @@ export function FunnelProvider({
|
||||
const funnel = data?.payload;
|
||||
const initialSteps = funnel?.steps?.length ? funnel.steps : initialStepsData;
|
||||
const [steps, setSteps] = useState<FunnelStepData[]>(initialSteps);
|
||||
const [triggerSave, setTriggerSave] = useState<boolean>(false);
|
||||
const [isUpdatingFunnel, setIsUpdatingFunnel] = useState<boolean>(false);
|
||||
const [lastUpdatedSteps, setLastUpdatedSteps] = useState<FunnelStepData[]>(
|
||||
initialSteps,
|
||||
);
|
||||
|
||||
// Check if there are unsaved changes by comparing with initial steps from API
|
||||
const hasUnsavedChanges = useMemo(() => {
|
||||
const normalizedCurrentSteps = normalizeSteps(steps);
|
||||
const normalizedInitialSteps = normalizeSteps(lastUpdatedSteps);
|
||||
return !isEqual(normalizedCurrentSteps, normalizedInitialSteps);
|
||||
}, [steps, lastUpdatedSteps]);
|
||||
|
||||
const { hasIncompleteStepFields, hasAllEmptyStepFields } = useMemo(
|
||||
() => ({
|
||||
hasAllEmptyStepFields: steps.every(
|
||||
@@ -98,15 +116,6 @@ export function FunnelProvider({
|
||||
[steps],
|
||||
);
|
||||
|
||||
const [unexecutedFunnels, setUnexecutedFunnels] = useLocalStorage<string[]>(
|
||||
LOCALSTORAGE.UNEXECUTED_FUNNELS,
|
||||
[],
|
||||
);
|
||||
|
||||
const [hasFunnelBeenExecuted, setHasFunnelBeenExecuted] = useState(
|
||||
!unexecutedFunnels.includes(funnelId),
|
||||
);
|
||||
|
||||
const {
|
||||
data: validationResponse,
|
||||
isLoading: isValidationLoading,
|
||||
@@ -116,7 +125,13 @@ export function FunnelProvider({
|
||||
selectedTime,
|
||||
startTime,
|
||||
endTime,
|
||||
enabled: !!funnelId && !!selectedTime && !!startTime && !!endTime,
|
||||
enabled:
|
||||
!!funnelId &&
|
||||
!!selectedTime &&
|
||||
!!startTime &&
|
||||
!!endTime &&
|
||||
!hasIncompleteStepFields,
|
||||
steps,
|
||||
});
|
||||
|
||||
const validTracesCount = useMemo(
|
||||
@@ -185,11 +200,7 @@ export function FunnelProvider({
|
||||
|
||||
const handleRunFunnel = useCallback(async (): Promise<void> => {
|
||||
if (validTracesCount === 0) return;
|
||||
if (!hasFunnelBeenExecuted) {
|
||||
setUnexecutedFunnels(unexecutedFunnels.filter((id) => id !== funnelId));
|
||||
|
||||
setHasFunnelBeenExecuted(true);
|
||||
}
|
||||
queryClient.refetchQueries([
|
||||
REACT_QUERY_KEY.GET_FUNNEL_OVERVIEW,
|
||||
funnelId,
|
||||
@@ -215,15 +226,13 @@ export function FunnelProvider({
|
||||
funnelId,
|
||||
selectedTime,
|
||||
]);
|
||||
}, [
|
||||
funnelId,
|
||||
hasFunnelBeenExecuted,
|
||||
unexecutedFunnels,
|
||||
queryClient,
|
||||
selectedTime,
|
||||
setUnexecutedFunnels,
|
||||
validTracesCount,
|
||||
]);
|
||||
}, [funnelId, queryClient, selectedTime, validTracesCount]);
|
||||
|
||||
const handleSaveFunnel = useCallback(() => {
|
||||
setTriggerSave(true);
|
||||
// Reset the trigger after a brief moment to allow useFunnelConfiguration to pick it up
|
||||
setTimeout(() => setTriggerSave(false), 100);
|
||||
}, []);
|
||||
|
||||
const value = useMemo<FunnelContextType>(
|
||||
() => ({
|
||||
@@ -239,14 +248,19 @@ export function FunnelProvider({
|
||||
handleAddStep: addNewStep,
|
||||
handleStepRemoval,
|
||||
handleRunFunnel,
|
||||
handleSaveFunnel,
|
||||
triggerSave,
|
||||
validationResponse,
|
||||
isValidateStepsLoading: isValidationLoading || isValidationFetching,
|
||||
hasIncompleteStepFields,
|
||||
hasAllEmptyStepFields,
|
||||
handleReplaceStep,
|
||||
handleRestoreSteps,
|
||||
hasFunnelBeenExecuted,
|
||||
setHasFunnelBeenExecuted,
|
||||
hasUnsavedChanges,
|
||||
setIsUpdatingFunnel,
|
||||
isUpdatingFunnel,
|
||||
lastUpdatedSteps,
|
||||
setLastUpdatedSteps,
|
||||
}),
|
||||
[
|
||||
funnelId,
|
||||
@@ -260,6 +274,8 @@ export function FunnelProvider({
|
||||
addNewStep,
|
||||
handleStepRemoval,
|
||||
handleRunFunnel,
|
||||
handleSaveFunnel,
|
||||
triggerSave,
|
||||
validationResponse,
|
||||
isValidationLoading,
|
||||
isValidationFetching,
|
||||
@@ -267,8 +283,11 @@ export function FunnelProvider({
|
||||
hasAllEmptyStepFields,
|
||||
handleReplaceStep,
|
||||
handleRestoreSteps,
|
||||
hasFunnelBeenExecuted,
|
||||
setHasFunnelBeenExecuted,
|
||||
hasUnsavedChanges,
|
||||
setIsUpdatingFunnel,
|
||||
isUpdatingFunnel,
|
||||
lastUpdatedSteps,
|
||||
setLastUpdatedSteps,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -4,11 +4,9 @@ import { Input } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { AxiosError } from 'axios';
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useCreateFunnel } from 'hooks/TracesFunnels/useFunnels';
|
||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { Check, X } from 'lucide-react';
|
||||
@@ -34,11 +32,6 @@ function CreateFunnel({
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const [unexecutedFunnels, setUnexecutedFunnels] = useLocalStorage<string[]>(
|
||||
LOCALSTORAGE.UNEXECUTED_FUNNELS,
|
||||
[],
|
||||
);
|
||||
|
||||
const handleCreate = (): void => {
|
||||
createFunnelMutation.mutate(
|
||||
{
|
||||
@@ -61,9 +54,6 @@ function CreateFunnel({
|
||||
queryClient.invalidateQueries([REACT_QUERY_KEY.GET_FUNNELS_LIST]);
|
||||
|
||||
const funnelId = data?.payload?.funnel_id;
|
||||
if (funnelId) {
|
||||
setUnexecutedFunnels([...unexecutedFunnels, funnelId]);
|
||||
}
|
||||
|
||||
onClose(funnelId);
|
||||
if (funnelId && redirectToDetails) {
|
||||
|
||||
@@ -2,13 +2,16 @@ import '../RenameFunnel/RenameFunnel.styles.scss';
|
||||
import './DeleteFunnel.styles.scss';
|
||||
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useDeleteFunnel } from 'hooks/TracesFunnels/useFunnels';
|
||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { Trash2, X } from 'lucide-react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { FunnelStepData } from 'types/api/traceFunnels';
|
||||
|
||||
interface DeleteFunnelProps {
|
||||
isOpen: boolean;
|
||||
@@ -29,6 +32,13 @@ function DeleteFunnel({
|
||||
|
||||
const history = useHistory();
|
||||
const { pathname } = history.location;
|
||||
|
||||
// localStorage hook for funnel steps
|
||||
const localStorageKey = `${LOCALSTORAGE.FUNNEL_STEPS}_${funnelId}`;
|
||||
const [, , clearLocalStorageSavedSteps] = useLocalStorage<
|
||||
FunnelStepData[] | null
|
||||
>(localStorageKey, null);
|
||||
|
||||
const handleDelete = (): void => {
|
||||
deleteFunnelMutation.mutate(
|
||||
{
|
||||
@@ -39,6 +49,7 @@ function DeleteFunnel({
|
||||
notifications.success({
|
||||
message: 'Funnel deleted successfully',
|
||||
});
|
||||
clearLocalStorageSavedSteps();
|
||||
onClose();
|
||||
|
||||
if (
|
||||
|
||||
12
go.mod
12
go.mod
@@ -8,7 +8,7 @@ require (
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.30.0
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
|
||||
github.com/SigNoz/signoz-otel-collector v0.111.39
|
||||
github.com/SigNoz/signoz-otel-collector v0.111.43
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||
github.com/antonmedv/expr v1.15.3
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
@@ -69,8 +69,8 @@ require (
|
||||
go.uber.org/multierr v1.11.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
||||
golang.org/x/oauth2 v0.24.0
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0
|
||||
golang.org/x/oauth2 v0.26.0
|
||||
golang.org/x/sync v0.14.0
|
||||
golang.org/x/text v0.25.0
|
||||
google.golang.org/protobuf v1.36.0
|
||||
@@ -125,7 +125,7 @@ require (
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-openapi/validate v0.24.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/goccy/go-json v0.10.4 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf // indirect
|
||||
@@ -182,7 +182,7 @@ require (
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.111.0 // indirect
|
||||
github.com/paulmach/orb v0.11.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
|
||||
@@ -267,7 +267,7 @@ require (
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
golang.org/x/tools v0.28.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
gonum.org/v1/gonum v0.15.1 // indirect
|
||||
google.golang.org/api v0.213.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241216192217-9240e9c98484 // indirect
|
||||
|
||||
24
go.sum
24
go.sum
@@ -100,8 +100,8 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkbj57eGXx8H3ZJ4zhmQXBnrW523ktj8=
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc=
|
||||
github.com/SigNoz/signoz-otel-collector v0.111.39 h1:Dl8QqZNAsj2atxP572OzsszPK0XPpd3LLPNPRAUJ5wo=
|
||||
github.com/SigNoz/signoz-otel-collector v0.111.39/go.mod h1:DCu/D+lqhsPNSGS4IMD+4gn7q06TGzOCKazSy+GURVc=
|
||||
github.com/SigNoz/signoz-otel-collector v0.111.43 h1:upWUoxDl5kCE/WI5+di2oqA/wJi2NU/PRyN8zDR078c=
|
||||
github.com/SigNoz/signoz-otel-collector v0.111.43/go.mod h1:iUGoKEaNQmLNptTwEz9o5kZ0ntbCMQsrV53Y2TDd1Qg=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
@@ -322,8 +322,8 @@ github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpG
|
||||
github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-zookeeper/zk v1.0.4 h1:DPzxraQx7OrPyXq2phlGlNSIyWEsAox0RJmjTseMV6I=
|
||||
github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
@@ -758,8 +758,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
@@ -1138,8 +1138,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -1241,8 +1241,8 @@ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -1425,8 +1425,8 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -12,4 +12,16 @@ type Analytics interface {
|
||||
|
||||
// Sends analytics messages to an analytics backend.
|
||||
Send(context.Context, ...analyticstypes.Message)
|
||||
|
||||
// Tracks an event on a group level. Input is group, event name, and attributes. The user is "stats_<org_id>".
|
||||
TrackGroup(context.Context, string, string, map[string]any)
|
||||
|
||||
// Tracks an event on a user level and attributes it with the group. Input is group, user, event name, and attributes.
|
||||
TrackUser(context.Context, string, string, string, map[string]any)
|
||||
|
||||
// Identifies a group. Input is group, traits.
|
||||
IdentifyGroup(context.Context, string, map[string]any)
|
||||
|
||||
// Identifies a user. Input is group, user, traits.
|
||||
IdentifyUser(context.Context, string, string, map[string]any)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,18 @@ func (provider *Provider) Start(_ context.Context) error {
|
||||
|
||||
func (provider *Provider) Send(ctx context.Context, messages ...analyticstypes.Message) {}
|
||||
|
||||
func (provider *Provider) TrackGroup(ctx context.Context, group, event string, attributes map[string]any) {
|
||||
}
|
||||
|
||||
func (provider *Provider) TrackUser(ctx context.Context, group, user, event string, attributes map[string]any) {
|
||||
}
|
||||
|
||||
func (provider *Provider) IdentifyGroup(ctx context.Context, group string, traits map[string]any) {
|
||||
}
|
||||
|
||||
func (provider *Provider) IdentifyUser(ctx context.Context, group, user string, traits map[string]any) {
|
||||
}
|
||||
|
||||
func (provider *Provider) Stop(_ context.Context) error {
|
||||
close(provider.stopC)
|
||||
return nil
|
||||
|
||||
@@ -27,7 +27,25 @@ func (provider *provider) Start(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) Send(ctx context.Context, messages ...analyticstypes.Message) {}
|
||||
func (provider *provider) Send(ctx context.Context, messages ...analyticstypes.Message) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func (provider *provider) TrackGroup(ctx context.Context, group, event string, attributes map[string]any) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func (provider *provider) TrackUser(ctx context.Context, group, user, event string, attributes map[string]any) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func (provider *provider) IdentifyGroup(ctx context.Context, group string, traits map[string]any) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func (provider *provider) IdentifyUser(ctx context.Context, group, user string, traits map[string]any) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func (provider *provider) Stop(_ context.Context) error {
|
||||
close(provider.stopC)
|
||||
|
||||
@@ -50,6 +50,100 @@ func (provider *provider) Send(ctx context.Context, messages ...analyticstypes.M
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *provider) TrackGroup(ctx context.Context, group, event string, properties map[string]any) {
|
||||
if properties == nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "empty attributes received, skipping event", "group", group, "event", event)
|
||||
return
|
||||
}
|
||||
|
||||
err := provider.client.Enqueue(analyticstypes.Track{
|
||||
UserId: "stats_" + group,
|
||||
Event: event,
|
||||
Properties: analyticstypes.NewPropertiesFromMap(properties),
|
||||
Context: &analyticstypes.Context{
|
||||
Extra: map[string]interface{}{
|
||||
analyticstypes.KeyGroupID: group,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *provider) TrackUser(ctx context.Context, group, user, event string, properties map[string]any) {
|
||||
if properties == nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "empty attributes received, skipping event", "user", user, "group", group, "event", event)
|
||||
return
|
||||
}
|
||||
|
||||
err := provider.client.Enqueue(analyticstypes.Track{
|
||||
UserId: user,
|
||||
Event: event,
|
||||
Properties: analyticstypes.NewPropertiesFromMap(properties),
|
||||
Context: &analyticstypes.Context{
|
||||
Extra: map[string]interface{}{
|
||||
analyticstypes.KeyGroupID: group,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *provider) IdentifyGroup(ctx context.Context, group string, traits map[string]any) {
|
||||
if traits == nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "empty attributes received, skipping identify", "group", group)
|
||||
return
|
||||
}
|
||||
|
||||
// identify the user
|
||||
err := provider.client.Enqueue(analyticstypes.Identify{
|
||||
UserId: "stats_" + group,
|
||||
Traits: analyticstypes.NewTraitsFromMap(traits),
|
||||
})
|
||||
if err != nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err)
|
||||
}
|
||||
|
||||
// identify the group using the stats user
|
||||
err = provider.client.Enqueue(analyticstypes.Group{
|
||||
UserId: "stats_" + group,
|
||||
GroupId: group,
|
||||
Traits: analyticstypes.NewTraitsFromMap(traits),
|
||||
})
|
||||
if err != nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *provider) IdentifyUser(ctx context.Context, group, user string, traits map[string]any) {
|
||||
if traits == nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "empty attributes received, skipping identify", "user", user, "group", group)
|
||||
return
|
||||
}
|
||||
|
||||
// identify the user
|
||||
err := provider.client.Enqueue(analyticstypes.Identify{
|
||||
UserId: user,
|
||||
Traits: analyticstypes.NewTraitsFromMap(traits),
|
||||
})
|
||||
if err != nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err)
|
||||
}
|
||||
|
||||
// associate the user with the group
|
||||
err = provider.client.Enqueue(analyticstypes.Group{
|
||||
UserId: user,
|
||||
GroupId: group,
|
||||
Traits: analyticstypes.NewTraits().Set("id", group), // A trait is required
|
||||
})
|
||||
if err != nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *provider) Stop(ctx context.Context) error {
|
||||
if err := provider.client.Close(); err != nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "unable to close segment client", "err", err)
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/analyticstypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
@@ -45,19 +44,7 @@ func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy s
|
||||
return nil, err
|
||||
}
|
||||
|
||||
module.analytics.Send(ctx,
|
||||
analyticstypes.Track{
|
||||
UserId: creator.String(),
|
||||
Event: "Dashboard Created",
|
||||
Properties: analyticstypes.NewPropertiesFromMap(dashboardtypes.NewStatsFromStorableDashboards([]*dashboardtypes.StorableDashboard{storableDashboard})),
|
||||
Context: &analyticstypes.Context{
|
||||
Extra: map[string]interface{}{
|
||||
analyticstypes.KeyGroupID: orgID,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
module.analytics.TrackUser(ctx, orgID.String(), creator.String(), "Dashboard Created", dashboardtypes.NewStatsFromStorableDashboards([]*dashboardtypes.StorableDashboard{storableDashboard}))
|
||||
return dashboard, nil
|
||||
}
|
||||
|
||||
|
||||
31
pkg/modules/user/impluser/getter.go
Normal file
31
pkg/modules/user/impluser/getter.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package impluser
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type getter struct {
|
||||
store types.UserStore
|
||||
}
|
||||
|
||||
func NewGetter(store types.UserStore) user.Getter {
|
||||
return &getter{store: store}
|
||||
}
|
||||
|
||||
func (module *getter) ListByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.User, error) {
|
||||
gettableUsers, err := module.store.ListUsers(ctx, orgID.StringValue())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]*types.User, len(gettableUsers))
|
||||
for i, user := range gettableUsers {
|
||||
users[i] = &user.User
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
@@ -326,7 +326,7 @@ func (h *handler) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
updatedUser, err := h.module.UpdateUser(ctx, claims.OrgID, id, &user)
|
||||
updatedUser, err := h.module.UpdateUser(ctx, claims.OrgID, id, &user, claims.UserID)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
@@ -347,7 +347,7 @@ func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.module.DeleteUser(ctx, claims.OrgID, id); err != nil {
|
||||
if err := h.module.DeleteUser(ctx, claims.OrgID, id, claims.UserID); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/analyticstypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/emailtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -135,35 +134,9 @@ func (m *Module) CreateUserWithPassword(ctx context.Context, user *types.User, p
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.analytics.Send(ctx,
|
||||
analyticstypes.Identify{
|
||||
UserId: user.ID.String(),
|
||||
Traits: analyticstypes.
|
||||
NewTraits().
|
||||
SetName(user.DisplayName).
|
||||
SetEmail(user.Email).
|
||||
Set("role", user.Role).
|
||||
SetCreatedAt(user.CreatedAt),
|
||||
},
|
||||
analyticstypes.Group{
|
||||
UserId: user.ID.String(),
|
||||
GroupId: user.OrgID,
|
||||
},
|
||||
analyticstypes.Track{
|
||||
UserId: user.ID.String(),
|
||||
Event: "User Created",
|
||||
Properties: analyticstypes.NewPropertiesFromMap(map[string]any{
|
||||
"role": user.Role,
|
||||
"email": user.Email,
|
||||
"name": user.DisplayName,
|
||||
}),
|
||||
Context: &analyticstypes.Context{
|
||||
Extra: map[string]interface{}{
|
||||
analyticstypes.KeyGroupID: user.OrgID,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
traitsOrProperties := types.NewTraitsFromUser(user)
|
||||
m.analytics.IdentifyUser(ctx, user.OrgID, user.ID.String(), traitsOrProperties)
|
||||
m.analytics.TrackUser(ctx, user.OrgID, user.ID.String(), "User Created", traitsOrProperties)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
@@ -173,35 +146,9 @@ func (m *Module) CreateUser(ctx context.Context, user *types.User) error {
|
||||
return err
|
||||
}
|
||||
|
||||
m.analytics.Send(ctx,
|
||||
analyticstypes.Identify{
|
||||
UserId: user.ID.String(),
|
||||
Traits: analyticstypes.
|
||||
NewTraits().
|
||||
SetName(user.DisplayName).
|
||||
SetEmail(user.Email).
|
||||
Set("role", user.Role).
|
||||
SetCreatedAt(user.CreatedAt),
|
||||
},
|
||||
analyticstypes.Group{
|
||||
UserId: user.ID.String(),
|
||||
GroupId: user.OrgID,
|
||||
},
|
||||
analyticstypes.Track{
|
||||
UserId: user.ID.String(),
|
||||
Event: "User Created",
|
||||
Properties: analyticstypes.NewPropertiesFromMap(map[string]any{
|
||||
"role": user.Role,
|
||||
"email": user.Email,
|
||||
"name": user.DisplayName,
|
||||
}),
|
||||
Context: &analyticstypes.Context{
|
||||
Extra: map[string]interface{}{
|
||||
analyticstypes.KeyGroupID: user.OrgID,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
traitsOrProperties := types.NewTraitsFromUser(user)
|
||||
m.analytics.IdentifyUser(ctx, user.OrgID, user.ID.String(), traitsOrProperties)
|
||||
m.analytics.TrackUser(ctx, user.OrgID, user.ID.String(), "User Created", traitsOrProperties)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -226,11 +173,22 @@ func (m *Module) ListUsers(ctx context.Context, orgID string) ([]*types.Gettable
|
||||
return m.store.ListUsers(ctx, orgID)
|
||||
}
|
||||
|
||||
func (m *Module) UpdateUser(ctx context.Context, orgID string, id string, user *types.User) (*types.User, error) {
|
||||
return m.store.UpdateUser(ctx, orgID, id, user)
|
||||
func (m *Module) UpdateUser(ctx context.Context, orgID string, id string, user *types.User, updatedBy string) (*types.User, error) {
|
||||
user, err := m.store.UpdateUser(ctx, orgID, id, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
traits := types.NewTraitsFromUser(user)
|
||||
m.analytics.IdentifyUser(ctx, user.OrgID, user.ID.String(), traits)
|
||||
|
||||
traits["updated_by"] = updatedBy
|
||||
m.analytics.TrackUser(ctx, user.OrgID, user.ID.String(), "User Updated", traits)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (m *Module) DeleteUser(ctx context.Context, orgID string, id string) error {
|
||||
func (m *Module) DeleteUser(ctx context.Context, orgID string, id string, deletedBy string) error {
|
||||
user, err := m.store.GetUserByID(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -250,7 +208,15 @@ func (m *Module) DeleteUser(ctx context.Context, orgID string, id string) error
|
||||
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot delete the last admin")
|
||||
}
|
||||
|
||||
return m.store.DeleteUser(ctx, orgID, user.ID.StringValue())
|
||||
if err := m.store.DeleteUser(ctx, orgID, user.ID.StringValue()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.analytics.TrackUser(ctx, user.OrgID, user.ID.String(), "User Deleted", map[string]any{
|
||||
"deleted_by": deletedBy,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Module) CreateResetPasswordToken(ctx context.Context, userID string) (*types.ResetPasswordRequest, error) {
|
||||
@@ -644,10 +610,16 @@ func (m *Module) Register(ctx context.Context, req *types.PostableRegisterOrgAnd
|
||||
}
|
||||
|
||||
func (m *Module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
|
||||
stats := make(map[string]any)
|
||||
count, err := m.store.CountByOrgID(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if err == nil {
|
||||
stats["user.count"] = count
|
||||
}
|
||||
|
||||
return map[string]any{"user.count": count}, nil
|
||||
count, err = m.store.CountAPIKeyByOrgID(ctx, orgID)
|
||||
if err == nil {
|
||||
stats["factor.api_key.count"] = count
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
@@ -826,3 +826,21 @@ func (store *store) CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64,
|
||||
|
||||
return int64(count), nil
|
||||
}
|
||||
|
||||
func (store *store) CountAPIKeyByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) {
|
||||
apiKey := new(types.StorableAPIKey)
|
||||
|
||||
count, err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(apiKey).
|
||||
Join("JOIN users ON users.id = storable_api_key.user_id").
|
||||
Where("org_id = ?", orgID).
|
||||
Count(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return int64(count), nil
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ type Module interface {
|
||||
GetUserByEmailInOrg(ctx context.Context, orgID string, email string) (*types.GettableUser, error)
|
||||
GetUsersByRoleInOrg(ctx context.Context, orgID string, role types.Role) ([]*types.GettableUser, error)
|
||||
ListUsers(ctx context.Context, orgID string) ([]*types.GettableUser, error)
|
||||
UpdateUser(ctx context.Context, orgID string, id string, user *types.User) (*types.User, error)
|
||||
DeleteUser(ctx context.Context, orgID string, id string) error
|
||||
UpdateUser(ctx context.Context, orgID string, id string, user *types.User, updatedBy string) (*types.User, error)
|
||||
DeleteUser(ctx context.Context, orgID string, id string, deletedBy string) error
|
||||
|
||||
// login
|
||||
GetAuthenticatedUser(ctx context.Context, orgID, email, password, refreshToken string) (*types.User, error)
|
||||
@@ -70,6 +70,11 @@ type Module interface {
|
||||
statsreporter.StatsCollector
|
||||
}
|
||||
|
||||
type Getter interface {
|
||||
// Get gets the users based on the given id
|
||||
ListByOrgID(context.Context, valuer.UUID) ([]*types.User, error)
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
// invite
|
||||
CreateInvite(http.ResponseWriter, *http.Request)
|
||||
|
||||
@@ -1584,6 +1584,7 @@ func (aH *APIHandler) registerEvent(w http.ResponseWriter, r *http.Request) {
|
||||
switch request.EventType {
|
||||
case model.TrackEvent:
|
||||
telemetry.GetInstance().SendEvent(request.EventName, request.Attributes, claims.Email, request.RateLimited, true)
|
||||
aH.Signoz.Analytics.TrackUser(r.Context(), claims.OrgID, claims.UserID, request.EventName, request.Attributes)
|
||||
case model.GroupEvent:
|
||||
telemetry.GetInstance().SendGroupEvent(request.Attributes, claims.Email)
|
||||
case model.IdentifyEvent:
|
||||
@@ -2023,7 +2024,7 @@ func (aH *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
_, errv2 := aH.Signoz.Modules.User.Register(r.Context(), &req)
|
||||
user, errv2 := aH.Signoz.Modules.User.Register(r.Context(), &req)
|
||||
if errv2 != nil {
|
||||
render.Error(w, errv2)
|
||||
return
|
||||
@@ -2033,7 +2034,7 @@ func (aH *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
|
||||
// from here onwards, we expect admin (owner) to invite other users.
|
||||
aH.SetupCompleted = true
|
||||
|
||||
aH.Respond(w, nil)
|
||||
aH.Respond(w, user)
|
||||
}
|
||||
|
||||
func handleSsoError(w http.ResponseWriter, r *http.Request, redirectURL string) {
|
||||
|
||||
@@ -65,11 +65,19 @@ func Parse(filters *v3.FilterSet) (string, error) {
|
||||
case v3.FilterOperatorExists, v3.FilterOperatorNotExists:
|
||||
// accustom log filters like `body.log.message EXISTS` into EXPR language
|
||||
// where User is attempting to check for keys present in JSON log body
|
||||
key, found := strings.CutPrefix(v.Key.Key, "body.")
|
||||
if found {
|
||||
filter = fmt.Sprintf("%s %s %s", exprFormattedValue(key), logOperatorsToExpr[v.Operator], "fromJSON(body)")
|
||||
if strings.HasPrefix(v.Key.Key, "body.") {
|
||||
filter = fmt.Sprintf("%s %s %s", exprFormattedValue(strings.TrimPrefix(v.Key.Key, "body.")), logOperatorsToExpr[v.Operator], "fromJSON(body)")
|
||||
} else if typ := getTypeName(v.Key.Type); typ != "" {
|
||||
filter = fmt.Sprintf("%s %s %s", exprFormattedValue(v.Key.Key), logOperatorsToExpr[v.Operator], typ)
|
||||
} else {
|
||||
filter = fmt.Sprintf("%s %s %s", exprFormattedValue(v.Key.Key), logOperatorsToExpr[v.Operator], getTypeName(v.Key.Type))
|
||||
// if type of key is not available; is considered as TOP LEVEL key in OTEL Log Data model hence
|
||||
// switch Exist and Not Exists operators with NOT EQUAL and EQUAL respectively
|
||||
operator := v3.FilterOperatorNotEqual
|
||||
if v.Operator == v3.FilterOperatorNotExists {
|
||||
operator = v3.FilterOperatorEqual
|
||||
}
|
||||
|
||||
filter = fmt.Sprintf("%s %s nil", v.Key.Key, logOperatorsToExpr[operator])
|
||||
}
|
||||
default:
|
||||
filter = fmt.Sprintf("%s %s %s", name, logOperatorsToExpr[v.Operator], exprFormattedValue(v.Value))
|
||||
|
||||
@@ -141,6 +141,34 @@ var testCases = []struct {
|
||||
}},
|
||||
Expr: `"key" not in attributes`,
|
||||
},
|
||||
{
|
||||
Name: "trace_id not exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
}},
|
||||
Expr: `trace_id == nil`,
|
||||
},
|
||||
{
|
||||
Name: "trace_id exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "trace_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
}},
|
||||
Expr: `trace_id != nil`,
|
||||
},
|
||||
{
|
||||
Name: "span_id not exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "span_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "nexists"},
|
||||
}},
|
||||
Expr: `span_id == nil`,
|
||||
},
|
||||
{
|
||||
Name: "span_id exists",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
{Key: v3.AttributeKey{Key: "span_id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "", Operator: "exists"},
|
||||
}},
|
||||
Expr: `span_id != nil`,
|
||||
},
|
||||
{
|
||||
Name: "Multi filter",
|
||||
Query: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/emailing/smtpemailing"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus/clickhouseprometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
@@ -153,9 +154,9 @@ func NewSharderProviderFactories() factory.NamedMap[factory.ProviderFactory[shar
|
||||
)
|
||||
}
|
||||
|
||||
func NewStatsReporterProviderFactories(telemetryStore telemetrystore.TelemetryStore, collectors []statsreporter.StatsCollector, orgGetter organization.Getter, build version.Build, analyticsConfig analytics.Config) factory.NamedMap[factory.ProviderFactory[statsreporter.StatsReporter, statsreporter.Config]] {
|
||||
func NewStatsReporterProviderFactories(telemetryStore telemetrystore.TelemetryStore, collectors []statsreporter.StatsCollector, orgGetter organization.Getter, userGetter user.Getter, build version.Build, analyticsConfig analytics.Config) factory.NamedMap[factory.ProviderFactory[statsreporter.StatsReporter, statsreporter.Config]] {
|
||||
return factory.MustNewNamedMap(
|
||||
analyticsstatsreporter.NewFactory(telemetryStore, collectors, orgGetter, build, analyticsConfig),
|
||||
analyticsstatsreporter.NewFactory(telemetryStore, collectors, orgGetter, userGetter, build, analyticsConfig),
|
||||
noopstatsreporter.NewFactory(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import (
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/SigNoz/signoz/pkg/analytics"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstoretest"
|
||||
"github.com/SigNoz/signoz/pkg/statsreporter"
|
||||
@@ -61,8 +63,9 @@ func TestNewProviderFactories(t *testing.T) {
|
||||
})
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
userGetter := impluser.NewGetter(impluser.NewStore(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual), instrumentationtest.New().ToProviderSettings()))
|
||||
orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual)), nil)
|
||||
telemetryStore := telemetrystoretest.New(telemetrystore.Config{Provider: "clickhouse"}, sqlmock.QueryMatcherEqual)
|
||||
NewStatsReporterProviderFactories(telemetryStore, []statsreporter.StatsCollector{}, orgGetter, version.Build{}, analytics.Config{Enabled: true})
|
||||
NewStatsReporterProviderFactories(telemetryStore, []statsreporter.StatsCollector{}, orgGetter, userGetter, version.Build{}, analytics.Config{Enabled: true})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/ruler"
|
||||
@@ -31,6 +32,7 @@ import (
|
||||
type SigNoz struct {
|
||||
*factory.Registry
|
||||
Instrumentation instrumentation.Instrumentation
|
||||
Analytics analytics.Analytics
|
||||
Cache cache.Cache
|
||||
Web web.Web
|
||||
SQLStore sqlstore.SQLStore
|
||||
@@ -212,6 +214,9 @@ func New(
|
||||
// Initialize organization getter
|
||||
orgGetter := implorganization.NewGetter(implorganization.NewStore(sqlstore), sharder)
|
||||
|
||||
// Initialize user getter
|
||||
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings))
|
||||
|
||||
// Initialize alertmanager from the available alertmanager provider factories
|
||||
alertmanager, err := factory.NewProviderFromNamedMap(
|
||||
ctx,
|
||||
@@ -267,7 +272,7 @@ func New(
|
||||
ctx,
|
||||
providerSettings,
|
||||
config.StatsReporter,
|
||||
NewStatsReporterProviderFactories(telemetrystore, statsCollectors, orgGetter, version.Info, config.Analytics),
|
||||
NewStatsReporterProviderFactories(telemetrystore, statsCollectors, orgGetter, userGetter, version.Info, config.Analytics),
|
||||
config.StatsReporter.Provider(),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -288,6 +293,7 @@ func New(
|
||||
|
||||
return &SigNoz{
|
||||
Registry: registry,
|
||||
Analytics: analytics,
|
||||
Instrumentation: instrumentation,
|
||||
Cache: cache,
|
||||
Web: web,
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
@@ -131,15 +130,17 @@ func (migration *updateAgents) Up(ctx context.Context, db *bun.DB) error {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
// get all org ids
|
||||
var orgIDs []string
|
||||
if err := tx.NewSelect().Model(new(types.Organization)).Column("id").Scan(ctx, &orgIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// there are multiple orgs, so we don't need to update the agents table
|
||||
if len(orgIDs) > 1 {
|
||||
return errors.Newf(errors.TypeInternal, errors.CodeInternal, "multiple orgs found: %v", orgIDs)
|
||||
var orgID string
|
||||
err = tx.
|
||||
NewSelect().
|
||||
ColumnExpr("id").
|
||||
Table("organizations").
|
||||
Limit(1).
|
||||
Scan(ctx, &orgID)
|
||||
if err != nil {
|
||||
if err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = migration.
|
||||
@@ -157,7 +158,7 @@ func (migration *updateAgents) Up(ctx context.Context, db *bun.DB) error {
|
||||
|
||||
if err == nil && len(existingAgents) > 0 {
|
||||
newAgents, err := migration.
|
||||
CopyOldAgentToNewAgent(ctx, tx, existingAgents, orgIDs[0])
|
||||
CopyOldAgentToNewAgent(ctx, tx, existingAgents, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -190,7 +191,7 @@ func (migration *updateAgents) Up(ctx context.Context, db *bun.DB) error {
|
||||
|
||||
if err == nil && len(existingAgentConfigVersions) > 0 {
|
||||
newAgentConfigVersions, err := migration.
|
||||
CopyOldAgentConfigVersionToNewAgentConfigVersion(ctx, tx, existingAgentConfigVersions, orgIDs[0])
|
||||
CopyOldAgentConfigVersionToNewAgentConfigVersion(ctx, tx, existingAgentConfigVersions, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -223,7 +224,7 @@ func (migration *updateAgents) Up(ctx context.Context, db *bun.DB) error {
|
||||
|
||||
if err == nil && len(existingAgentConfigElements) > 0 {
|
||||
newAgentConfigElements, err := migration.
|
||||
CopyOldAgentConfigElementToNewAgentConfigElement(ctx, tx, existingAgentConfigElements, orgIDs[0])
|
||||
CopyOldAgentConfigElementToNewAgentConfigElement(ctx, tx, existingAgentConfigElements, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/analytics/segmentanalytics"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/statsreporter"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/analyticstypes"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
)
|
||||
@@ -32,6 +33,9 @@ type provider struct {
|
||||
// used to get organizations
|
||||
orgGetter organization.Getter
|
||||
|
||||
// used to get users
|
||||
userGetter user.Getter
|
||||
|
||||
// used to send stats to an analytics backend
|
||||
analytics analytics.Analytics
|
||||
|
||||
@@ -45,9 +49,9 @@ type provider struct {
|
||||
stopC chan struct{}
|
||||
}
|
||||
|
||||
func NewFactory(telemetryStore telemetrystore.TelemetryStore, collectors []statsreporter.StatsCollector, orgGetter organization.Getter, build version.Build, analyticsConfig analytics.Config) factory.ProviderFactory[statsreporter.StatsReporter, statsreporter.Config] {
|
||||
func NewFactory(telemetryStore telemetrystore.TelemetryStore, collectors []statsreporter.StatsCollector, orgGetter organization.Getter, userGetter user.Getter, build version.Build, analyticsConfig analytics.Config) factory.ProviderFactory[statsreporter.StatsReporter, statsreporter.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("analytics"), func(ctx context.Context, settings factory.ProviderSettings, config statsreporter.Config) (statsreporter.StatsReporter, error) {
|
||||
return New(ctx, settings, config, telemetryStore, collectors, orgGetter, build, analyticsConfig)
|
||||
return New(ctx, settings, config, telemetryStore, collectors, orgGetter, userGetter, build, analyticsConfig)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -58,6 +62,7 @@ func New(
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
collectors []statsreporter.StatsCollector,
|
||||
orgGetter organization.Getter,
|
||||
userGetter user.Getter,
|
||||
build version.Build,
|
||||
analyticsConfig analytics.Config,
|
||||
) (statsreporter.StatsReporter, error) {
|
||||
@@ -74,6 +79,7 @@ func New(
|
||||
telemetryStore: telemetryStore,
|
||||
collectors: collectors,
|
||||
orgGetter: orgGetter,
|
||||
userGetter: userGetter,
|
||||
analytics: analytics,
|
||||
build: build,
|
||||
deployment: deployment,
|
||||
@@ -116,6 +122,7 @@ func (provider *provider) Report(ctx context.Context) error {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add build and deployment stats
|
||||
stats["build.version"] = provider.build.Version()
|
||||
stats["build.branch"] = provider.build.Branch()
|
||||
stats["build.hash"] = provider.build.Hash()
|
||||
@@ -125,37 +132,30 @@ func (provider *provider) Report(ctx context.Context) error {
|
||||
stats["deployment.os"] = provider.deployment.OS()
|
||||
stats["deployment.arch"] = provider.deployment.Arch()
|
||||
|
||||
// Add org stats
|
||||
stats["display_name"] = org.DisplayName
|
||||
stats["name"] = org.Name
|
||||
stats["created_at"] = org.CreatedAt
|
||||
stats["alias"] = org.Alias
|
||||
|
||||
provider.settings.Logger().DebugContext(ctx, "reporting stats", "stats", stats)
|
||||
provider.analytics.Send(
|
||||
ctx,
|
||||
analyticstypes.Track{
|
||||
UserId: "stats_" + org.ID.String(),
|
||||
Event: "Stats Reported",
|
||||
Properties: analyticstypes.NewPropertiesFromMap(stats),
|
||||
Context: &analyticstypes.Context{
|
||||
Extra: map[string]interface{}{
|
||||
analyticstypes.KeyGroupID: org.ID.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
analyticstypes.Group{
|
||||
UserId: "stats_" + org.ID.String(),
|
||||
GroupId: org.ID.String(),
|
||||
Traits: analyticstypes.
|
||||
NewTraitsFromMap(stats).
|
||||
SetName(org.DisplayName).
|
||||
SetUsername(org.Name).
|
||||
SetCreatedAt(org.CreatedAt),
|
||||
},
|
||||
analyticstypes.Identify{
|
||||
UserId: "stats_" + org.ID.String(),
|
||||
Traits: analyticstypes.
|
||||
NewTraits().
|
||||
SetName(org.DisplayName).
|
||||
SetUsername(org.Name).
|
||||
SetCreatedAt(org.CreatedAt),
|
||||
},
|
||||
)
|
||||
|
||||
provider.analytics.IdentifyGroup(ctx, org.ID.String(), stats)
|
||||
provider.analytics.TrackGroup(ctx, org.ID.String(), "Stats Reported", stats)
|
||||
|
||||
if !provider.config.Collect.Identities {
|
||||
continue
|
||||
}
|
||||
|
||||
users, err := provider.userGetter.ListByOrgID(ctx, org.ID)
|
||||
if err != nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "failed to list users", "error", err, "org_id", org.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
provider.analytics.IdentifyUser(ctx, org.ID.String(), user.ID.String(), types.NewTraitsFromUser(user))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -12,6 +12,13 @@ type Config struct {
|
||||
|
||||
// Interval is the interval at which the stats are collected.
|
||||
Interval time.Duration `mapstructure:"interval"`
|
||||
|
||||
// Collect is the collection configuration.
|
||||
Collect Collect `mapstructure:"collect"`
|
||||
}
|
||||
|
||||
type Collect struct {
|
||||
Identities bool `mapstructure:"identities"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
@@ -22,6 +29,9 @@ func newConfig() factory.Config {
|
||||
return Config{
|
||||
Enabled: true,
|
||||
Interval: 6 * time.Hour,
|
||||
Collect: Collect{
|
||||
Identities: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,15 @@ func NewOrganizationKey(orgID valuer.UUID) uint32 {
|
||||
return hasher.Sum32()
|
||||
}
|
||||
|
||||
func NewTraitsFromOrganization(org *Organization) map[string]any {
|
||||
return map[string]any{
|
||||
"display_name": org.DisplayName,
|
||||
"name": org.Name,
|
||||
"created_at": org.CreatedAt,
|
||||
"alias": org.Alias,
|
||||
}
|
||||
}
|
||||
|
||||
type TTLSetting struct {
|
||||
bun.BaseModel `bun:"table:ttl_setting"`
|
||||
Identifiable
|
||||
|
||||
@@ -72,6 +72,7 @@ type UserStore interface {
|
||||
ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*StorableAPIKeyUser, error)
|
||||
RevokeAPIKey(ctx context.Context, id valuer.UUID, revokedByUserID valuer.UUID) error
|
||||
GetAPIKey(ctx context.Context, orgID, id valuer.UUID) (*StorableAPIKeyUser, error)
|
||||
CountAPIKeyByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error)
|
||||
|
||||
CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error)
|
||||
}
|
||||
@@ -254,3 +255,13 @@ type GettableLoginPrecheck struct {
|
||||
SelectOrg bool `json:"selectOrg"`
|
||||
Orgs []string `json:"orgs"`
|
||||
}
|
||||
|
||||
func NewTraitsFromUser(user *User) map[string]any {
|
||||
return map[string]any{
|
||||
"name": user.DisplayName,
|
||||
"role": user.Role,
|
||||
"email": user.Email,
|
||||
"display_name": user.DisplayName,
|
||||
"created_at": user.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user