Compare commits

..

2 Commits

Author SHA1 Message Date
SagarRajput-7
520a3f93c6 feat: research on data limit for signoz dashboard perf 2025-06-02 13:27:18 +05:30
Shivanshu Raj Shrivastava
f9cb9f10be feat: adds a part of trace funnel feature (APIs, module, handler, store, migrations) implementation (#7763)
* feat: adds server and handler changes

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* feat: add tracefunnel module and handler

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* feat: add required types for tracefunnels

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* feat: db operations, module and handler implementation

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* feat: add db migrations

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: add utility functions

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* test: add utility function tests

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* test: add handler tests

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* test: add trace funnel module tests

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: refactor handler and utils

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: add funnel validation while processing funnel steps

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* test: add more tests to utils

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: fix package naming

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: fix naming convention

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: update normalize funnel steps

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: added some improvements

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* fix: optimize funnel creation by combining insert and update operations

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: fix error handling

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* feat: trace funnel state management

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* fix: updated unit tests and mocks

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* fix: review comments

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* fix: minor fixes

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* fix: update funnel migration number

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* fix: review comments and some changes

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* fix: update modules

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

---------

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 07:00:49 +00:00
26 changed files with 690 additions and 1671 deletions

View File

@@ -167,8 +167,8 @@ interface UpdateFunnelDescriptionPayload {
export const saveFunnelDescription = async (
payload: UpdateFunnelDescriptionPayload,
): Promise<SuccessResponse<FunnelData> | ErrorResponse> => {
const response: AxiosResponse = await axios.put(
`${FUNNELS_BASE_PATH}/${payload.funnel_id}`,
const response: AxiosResponse = await axios.post(
`${FUNNELS_BASE_PATH}/save`,
payload,
);

View File

@@ -0,0 +1,85 @@
.custom-data-controls {
margin: 8px 0;
.custom-data-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.custom-data-inputs {
margin-top: 8px;
.input-group {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.input-label {
margin-right: 8px;
}
}
}
}
.global-custom-data-controls {
background-color: #f0f2f5;
border-radius: 4px;
padding: 10px;
margin-bottom: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
.ant-card-body {
padding: 12px;
}
&.dark-mode {
background-color: #141414;
border: 1px solid #303030;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
&.inline-layout {
.custom-data-inline-container {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 16px;
.title-switch-group {
display: flex;
align-items: center;
gap: 12px;
min-width: 180px;
.ant-typography {
white-space: nowrap;
}
}
.inputs-container {
display: flex;
align-items: center;
gap: 20px;
.input-group {
display: flex;
align-items: center;
margin-bottom: 0;
.input-label {
margin-right: 8px;
white-space: nowrap;
}
}
}
}
// Reduce vertical padding for the card
.ant-card-body {
padding: 8px 12px;
}
}
}

View File

@@ -0,0 +1,75 @@
import './CustomDataControls.styles.scss';
import { Card, InputNumber, Switch, Typography } from 'antd';
import { Widgets } from 'types/api/dashboard/getAll';
interface CustomDataControlsProps {
widget: Widgets;
onUpdate: (updatedWidget: Partial<Widgets>) => void;
}
const { Text } = Typography;
function CustomDataControls({
widget,
onUpdate,
}: CustomDataControlsProps): JSX.Element {
const handleCustomDataModeChange = (checked: boolean): void => {
onUpdate({
customDataMode: checked,
customXData: checked ? widget.customXData || 15 : undefined,
customYData: checked ? widget.customYData || 4 : undefined,
});
};
const handleDataPointsChange = (value: number | null): void => {
if (value !== null && value > 0) {
onUpdate({ customXData: value });
}
};
const handleSeriesCountChange = (value: number | null): void => {
if (value !== null && value > 0) {
onUpdate({ customYData: value });
}
};
return (
<Card className="custom-data-controls" size="small">
<div className="custom-data-header">
<Text strong>Custom Data Generator</Text>
<Switch
checked={widget.customDataMode || false}
onChange={handleCustomDataModeChange}
size="small"
/>
</div>
{widget.customDataMode && (
<div className="custom-data-inputs">
<div className="input-group">
<Text className="input-label">Data Points (X):</Text>
<InputNumber
value={widget.customXData || 15}
onChange={handleDataPointsChange}
size="small"
style={{ width: 80 }}
/>
</div>
<div className="input-group">
<Text className="input-label">Series Count (Y):</Text>
<InputNumber
value={widget.customYData || 4}
onChange={handleSeriesCountChange}
size="small"
style={{ width: 80 }}
/>
</div>
</div>
)}
</Card>
);
}
export default CustomDataControls;

View File

@@ -0,0 +1,91 @@
import './CustomDataControls.styles.scss';
import { Card, InputNumber, Switch, Typography } from 'antd';
interface GlobalCustomDataControlsProps {
customDataMode: boolean;
setCustomDataMode: (value: boolean) => void;
customXData: number;
setCustomXData: (value: number) => void;
customYData: number;
setCustomYData: (value: number) => void;
}
const { Text } = Typography;
function GlobalCustomDataControls({
customDataMode,
setCustomDataMode,
customXData,
setCustomXData,
customYData,
setCustomYData,
}: GlobalCustomDataControlsProps): JSX.Element {
const handleCustomDataModeChange = (checked: boolean): void => {
setCustomDataMode(checked);
// Set default values if not already set
if (checked) {
if (!customXData) setCustomXData(15);
if (!customYData) setCustomYData(4);
}
};
const handleDataPointsChange = (value: number | null): void => {
if (value !== null && value > 0) {
setCustomXData(value);
}
};
const handleSeriesCountChange = (value: number | null): void => {
if (value !== null && value > 0) {
setCustomYData(value);
}
};
return (
<Card
className="custom-data-controls global-custom-data-controls"
size="small"
>
<div className="custom-data-header">
<Text strong>Global Custom Data Generator</Text>
<Switch
checked={customDataMode}
onChange={handleCustomDataModeChange}
size="small"
/>
</div>
{customDataMode && (
<div className="custom-data-inputs">
<div className="input-group">
<Text className="input-label">Data Points (X):</Text>
<InputNumber
value={customXData || 15}
onChange={handleDataPointsChange}
size="small"
style={{ width: 80 }}
min={1}
max={1000}
/>
</div>
<div className="input-group">
<Text className="input-label">Series Count (Y):</Text>
<InputNumber
value={customYData || 4}
onChange={handleSeriesCountChange}
size="small"
style={{ width: 80 }}
min={1}
max={20}
/>
</div>
</div>
)}
</Card>
);
}
export default GlobalCustomDataControls;

View File

@@ -0,0 +1,88 @@
import './CustomDataControls.styles.scss';
import { Card, InputNumber, Switch, Typography } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useDashboard } from 'providers/Dashboard/Dashboard';
const { Text } = Typography;
function GlobalCustomDataControlsHeader(): JSX.Element {
const {
globalCustomDataMode,
setGlobalCustomDataMode,
globalCustomXData,
setGlobalCustomXData,
globalCustomYData,
setGlobalCustomYData,
} = useDashboard();
const isDarkMode = useIsDarkMode();
const handleCustomDataModeChange = (checked: boolean): void => {
setGlobalCustomDataMode(checked);
// Set default values if not already set
if (checked) {
if (!globalCustomXData) setGlobalCustomXData(15);
if (!globalCustomYData) setGlobalCustomYData(4);
}
};
const handleDataPointsChange = (value: number | null): void => {
if (value !== null && value > 0) {
setGlobalCustomXData(value);
}
};
const handleSeriesCountChange = (value: number | null): void => {
if (value !== null && value > 0) {
setGlobalCustomYData(value);
}
};
return (
<Card
className={`custom-data-controls global-custom-data-controls inline-layout ${
isDarkMode ? 'dark-mode' : ''
}`}
size="small"
>
<div className="custom-data-inline-container">
<div className="title-switch-group">
<Text strong>Custom Data Generator</Text>
<Switch
checked={globalCustomDataMode}
onChange={handleCustomDataModeChange}
size="small"
/>
</div>
{globalCustomDataMode && (
<div className="inputs-container">
<div className="input-group">
<Text className="input-label">Points (X):</Text>
<InputNumber
value={globalCustomXData || 15}
onChange={handleDataPointsChange}
size="small"
style={{ width: 80 }}
/>
</div>
<div className="input-group">
<Text className="input-label">Series (Y):</Text>
<InputNumber
value={globalCustomYData || 4}
onChange={handleSeriesCountChange}
size="small"
style={{ width: 80 }}
/>
</div>
</div>
)}
</div>
</Card>
);
}
export default GlobalCustomDataControlsHeader;

View File

@@ -121,6 +121,10 @@
}
}
.global-custom-data-section {
margin-bottom: 16px;
}
.dashboard-details {
display: flex;
justify-content: space-between;

View File

@@ -12,6 +12,7 @@ import {
Typography,
} from 'antd';
import logEvent from 'api/common/logEvent';
import GlobalCustomDataControlsHeader from 'components/CustomDataControls/GlobalCustomDataControlsHeader';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
@@ -478,6 +479,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
)}
</div>
</section>
<section className="global-custom-data-section">
<GlobalCustomDataControlsHeader />
</section>
{(tags?.length || 0) > 0 && (
<div className="dashboard-tags">
{tags?.map((tag) => (

View File

@@ -12,6 +12,7 @@ import {
Switch,
Typography,
} from 'antd';
import CustomDataControls from 'components/CustomDataControls/CustomDataControls';
import TimePreference from 'components/TimePreferenceDropDown';
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
import GraphTypes, {
@@ -113,6 +114,12 @@ function RightContainer({
customLegendColors,
setCustomLegendColors,
queryResponse,
customDataMode,
setCustomDataMode,
customXData,
setCustomXData,
customYData,
setCustomYData,
}: RightContainerProps): JSX.Element {
const { selectedDashboard } = useDashboard();
const [inputValue, setInputValue] = useState(title);
@@ -280,6 +287,30 @@ function RightContainer({
rootClassName="description-input"
/>
</section>
{/* Custom Data Controls */}
{selectedWidget && (
<CustomDataControls
widget={{
...selectedWidget,
customDataMode,
customXData,
customYData,
}}
onUpdate={(updatedWidget: Partial<Widgets>): void => {
if (updatedWidget.customDataMode !== undefined) {
setCustomDataMode(updatedWidget.customDataMode);
}
if (updatedWidget.customXData !== undefined) {
setCustomXData(updatedWidget.customXData);
}
if (updatedWidget.customYData !== undefined) {
setCustomYData(updatedWidget.customYData);
}
}}
/>
)}
<section className="panel-config">
<Typography.Text className="typography">Panel Type</Typography.Text>
<Select
@@ -554,6 +585,12 @@ interface RightContainerProps {
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
customDataMode: boolean;
setCustomDataMode: Dispatch<SetStateAction<boolean>>;
customXData: number;
setCustomXData: Dispatch<SetStateAction<number>>;
customYData: number;
setCustomYData: Dispatch<SetStateAction<number>>;
}
RightContainer.defaultProps = {

View File

@@ -239,6 +239,17 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
selectedWidget?.columnUnits || {},
);
// Custom data generator state
const [customDataMode, setCustomDataMode] = useState<boolean>(
selectedWidget?.customDataMode || false,
);
const [customXData, setCustomXData] = useState<number>(
selectedWidget?.customXData || 15,
);
const [customYData, setCustomYData] = useState<number>(
selectedWidget?.customYData || 4,
);
useEffect(() => {
setSelectedWidget((prev) => {
if (!prev) {
@@ -268,6 +279,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
legendPosition,
customLegendColors,
columnWidths: columnWidths?.[selectedWidget?.id],
customDataMode,
customXData,
customYData,
};
});
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -294,6 +308,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
legendPosition,
customLegendColors,
columnWidths,
customDataMode,
customXData,
customYData,
]);
const closeModal = (): void => {
@@ -503,6 +520,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
selectedTracesFields: selectedWidget?.selectedTracesFields || [],
legendPosition: selectedWidget?.legendPosition || LegendPosition.BOTTOM,
customLegendColors: selectedWidget?.customLegendColors || {},
customDataMode: selectedWidget?.customDataMode || false,
customXData: selectedWidget?.customXData || 15,
customYData: selectedWidget?.customYData || 4,
},
]
: [
@@ -532,6 +552,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
selectedTracesFields: selectedWidget?.selectedTracesFields || [],
legendPosition: selectedWidget?.legendPosition || LegendPosition.BOTTOM,
customLegendColors: selectedWidget?.customLegendColors || {},
customDataMode: selectedWidget?.customDataMode || false,
customXData: selectedWidget?.customXData || 15,
customYData: selectedWidget?.customYData || 4,
},
...afterWidgets,
],
@@ -796,6 +819,12 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
setSoftMin={setSoftMin}
softMax={softMax}
setSoftMax={setSoftMax}
customDataMode={customDataMode}
setCustomDataMode={setCustomDataMode}
customXData={customXData}
setCustomXData={setCustomXData}
customYData={customYData}
setCustomYData={setCustomYData}
/>
</OverlayScrollbar>
</RightContainerWrapper>

View File

@@ -10,12 +10,19 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import {
getCustomApiResponse,
getCustomChartData,
} from 'lib/uPlotLib/utils/getCustomChartData';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { cloneDeep, isEqual, isUndefined } from 'lodash-es';
import _noop from 'lodash-es/noop';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useTimezone } from 'providers/Timezone';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import uPlot from 'uplot';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
@@ -35,7 +42,14 @@ function UplotPanelWrapper({
customTooltipElement,
customSeries,
}: PanelWrapperProps): JSX.Element {
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const {
toScrollWidgetId,
setToScrollWidgetId,
globalCustomDataMode,
globalCustomXData,
globalCustomYData,
} = useDashboard();
const isDarkMode = useIsDarkMode();
const lineChartRef = useRef<ToggleGraphProps>();
const graphRef = useRef<HTMLDivElement>(null);
@@ -43,6 +57,11 @@ function UplotPanelWrapper({
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const { currentQuery } = useQueryBuilder();
// Get global time for custom data generation
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [hiddenGraph, setHiddenGraph] = useState<{ [key: string]: boolean }>();
useEffect(() => {
@@ -56,12 +75,64 @@ function UplotPanelWrapper({
}
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
// Create custom response when custom data mode is enabled (either via widget or global setting)
const effectiveQueryResponse = useMemo(() => {
// If global custom data mode is enabled, it should override widget settings
const isCustomDataEnabled = globalCustomDataMode || widget.customDataMode;
// When global custom data is enabled, use global values regardless of widget settings
let xData = null;
let yData = null;
if (globalCustomDataMode) {
// Global settings override widget settings
xData = globalCustomXData;
yData = globalCustomYData;
} else if (widget.customDataMode) {
// Only use widget settings if global is not enabled
xData = widget.customXData;
yData = widget.customYData;
}
if (isCustomDataEnabled && xData && yData) {
// Convert nanoseconds to seconds for custom data generation
const startTimeSeconds = Math.floor(minTime / 1000000000);
const endTimeSeconds = Math.floor(maxTime / 1000000000);
const customResponse = getCustomApiResponse(
xData,
yData,
startTimeSeconds,
endTimeSeconds,
);
// Return a properly structured response that matches the expected type
return {
...queryResponse,
data: customResponse,
isSuccess: true,
isError: false,
isLoading: false,
} as typeof queryResponse;
}
return queryResponse;
}, [
queryResponse,
widget.customDataMode,
widget.customXData,
widget.customYData,
globalCustomDataMode,
globalCustomXData,
globalCustomYData,
minTime,
maxTime,
]);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(queryResponse);
const { startTime, endTime } = getTimeRange(effectiveQueryResponse);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [queryResponse]);
}, [effectiveQueryResponse]);
const containerDimensions = useResizeObserver(graphRef);
@@ -69,28 +140,71 @@ function UplotPanelWrapper({
const {
graphVisibilityStates: localStoredVisibilityState,
} = getLocalStorageGraphVisibilityState({
apiResponse: queryResponse.data?.payload.data.result || [],
apiResponse: effectiveQueryResponse.data?.payload.data.result || [],
name: widget.id,
});
if (setGraphVisibility) {
setGraphVisibility(localStoredVisibilityState);
}
}, [queryResponse.data?.payload.data.result, setGraphVisibility, widget.id]);
}, [
effectiveQueryResponse.data?.payload.data.result,
setGraphVisibility,
widget.id,
]);
if (queryResponse.data && widget.panelTypes === PANEL_TYPES.BAR) {
if (effectiveQueryResponse.data && widget.panelTypes === PANEL_TYPES.BAR) {
const sortedSeriesData = getSortedSeriesData(
queryResponse.data?.payload.data.result,
effectiveQueryResponse.data?.payload.data.result,
);
// eslint-disable-next-line no-param-reassign
queryResponse.data.payload.data.result = sortedSeriesData;
effectiveQueryResponse.data.payload.data.result = sortedSeriesData;
}
const chartData = getUPlotChartData(
queryResponse?.data?.payload,
const chartData = useMemo(() => {
// If global custom data mode is enabled, it should override widget settings
const isCustomDataEnabled = globalCustomDataMode || widget.customDataMode;
// When global custom data is enabled, use global values regardless of widget settings
let xData = null;
let yData = null;
if (globalCustomDataMode) {
// Global settings override widget settings
xData = globalCustomXData;
yData = globalCustomYData;
} else if (widget.customDataMode) {
// Only use widget settings if global is not enabled
xData = widget.customXData;
yData = widget.customYData;
}
if (isCustomDataEnabled && xData && yData) {
// Convert nanoseconds to seconds for custom data generation
const startTimeSeconds = Math.floor(minTime / 1000000000);
const endTimeSeconds = Math.floor(maxTime / 1000000000);
return getCustomChartData(xData, yData, startTimeSeconds, endTimeSeconds);
}
return getUPlotChartData(
effectiveQueryResponse?.data?.payload,
widget.fillSpans,
widget?.stackedBarChart,
hiddenGraph,
);
}, [
widget.customDataMode,
widget.customXData,
widget.customYData,
globalCustomDataMode,
globalCustomXData,
globalCustomYData,
minTime,
maxTime,
effectiveQueryResponse?.data?.payload,
widget.fillSpans,
widget?.stackedBarChart,
hiddenGraph,
);
]);
useEffect(() => {
if (widget.panelTypes === PANEL_TYPES.BAR && widget?.stackedBarChart) {
@@ -110,16 +224,22 @@ function UplotPanelWrapper({
const { timezone } = useTimezone();
// Standard click handler without performance monitoring
const enhancedClickHandler = useMemo(() => {
if (!onClickHandler) return _noop;
return onClickHandler;
}, [onClickHandler]);
const options = useMemo(
() =>
getUPlotChartOptions({
id: widget?.id,
apiResponse: queryResponse.data?.payload,
apiResponse: effectiveQueryResponse.data?.payload,
dimensions: containerDimensions,
isDarkMode,
onDragSelect,
yAxisUnit: widget?.yAxisUnit,
onClickHandler: onClickHandler || _noop,
onClickHandler: enhancedClickHandler,
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
@@ -150,11 +270,11 @@ function UplotPanelWrapper({
widget.softMin,
widget.panelTypes,
widget?.stackedBarChart,
queryResponse.data?.payload,
effectiveQueryResponse.data?.payload,
containerDimensions,
isDarkMode,
onDragSelect,
onClickHandler,
enhancedClickHandler,
minTimeScale,
maxTimeScale,
graphVisibility,
@@ -171,6 +291,14 @@ function UplotPanelWrapper({
],
);
console.log(
'chartData',
'x-axis',
chartData[0].length,
'y-axis',
chartData.length,
);
return (
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
<Uplot options={options} data={chartData} ref={lineChartRef} />
@@ -183,7 +311,10 @@ function UplotPanelWrapper({
)}
{isFullViewMode && setGraphVisibility && !widget?.stackedBarChart && (
<GraphManager
data={getUPlotChartData(queryResponse?.data?.payload, widget.fillSpans)}
data={getUPlotChartData(
effectiveQueryResponse?.data?.payload,
widget.fillSpans,
)}
name={widget.id}
options={options}
yAxisUnit={widget.yAxisUnit}

View File

@@ -149,28 +149,30 @@ function SpanOverview({
<Typography.Text className="service-name">
{span.serviceName}
</Typography.Text>
{!!span.serviceName && !!span.name && (
<div className="add-funnel-button">
<span className="add-funnel-button__separator">·</span>
<Button
type="text"
size="small"
className="add-funnel-button__button"
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
handleAddSpanToFunnel(span);
}}
icon={
<img
className="add-funnel-button__icon"
src="/Icons/funnel-add.svg"
alt="funnel-icon"
/>
}
/>
</div>
)}
{!!span.serviceName &&
!!span.name &&
process.env.NODE_ENV === 'development' && (
<div className="add-funnel-button">
<span className="add-funnel-button__separator">·</span>
<Button
type="text"
size="small"
className="add-funnel-button__button"
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
handleAddSpanToFunnel(span);
}}
icon={
<img
className="add-funnel-button__icon"
src="/Icons/funnel-add.svg"
alt="funnel-icon"
/>
}
/>
</div>
)}
</section>
</div>
</div>
@@ -448,7 +450,7 @@ function Success(props: ISuccessProps): JSX.Element {
virtualiserRef={virtualizerRef}
setColumnWidths={setTraceFlamegraphStatsWidth}
/>
{selectedSpanToAddToFunnel && (
{selectedSpanToAddToFunnel && process.env.NODE_ENV === 'development' && (
<AddSpanToFunnelModal
span={selectedSpanToAddToFunnel}
isOpen={isAddSpanToFunnelModalOpen}

View File

@@ -142,7 +142,6 @@ export const useValidateFunnelSteps = ({
interface SaveFunnelDescriptionPayload {
funnel_id: string;
description: string;
timestamp: number;
}
export const useSaveFunnelDescription = (): UseMutationResult<
@@ -150,11 +149,7 @@ export const useSaveFunnelDescription = (): UseMutationResult<
Error,
SaveFunnelDescriptionPayload
> =>
useMutation<
SuccessResponse<FunnelData> | ErrorResponse,
Error,
SaveFunnelDescriptionPayload
>({
useMutation({
mutationFn: saveFunnelDescription,
});

View File

@@ -431,9 +431,10 @@ export const getUPlotChartOptions = ({
// Add single global cleanup listener for this chart
const globalCleanupHandler = (e: MouseEvent): void => {
const target = e.target as HTMLElement;
console.log('target', target);
if (
!target.closest('.u-legend') &&
!target.classList.contains('legend-tooltip')
!target?.closest?.('.u-legend') &&
!target?.classList?.contains('legend-tooltip')
) {
cleanupAllTooltips();
}

View File

@@ -67,15 +67,19 @@ export default function TraceDetailsPage(): JSX.Element {
key: 'trace-details',
children: <TraceDetailsV2 />,
},
{
label: (
<div className="tab-item">
<Cone className="funnel-icon" size={16} /> Funnels
</div>
),
key: 'funnels',
children: <div />,
},
...(process.env.NODE_ENV === 'development'
? [
{
label: (
<div className="tab-item">
<Cone className="funnel-icon" size={16} /> Funnels
</div>
),
key: 'funnels',
children: <div />,
},
]
: []),
{
label: (
<div className="tab-item">

View File

@@ -41,7 +41,6 @@ function AddFunnelDescriptionModal({
{
funnel_id: funnelId,
description,
timestamp: Date.now(),
},
{
onSuccess: () => {

View File

@@ -1,11 +1,13 @@
import './FunnelStep.styles.scss';
import { Button, Divider, Form, Switch, Tooltip } from 'antd';
import { Button, Divider, Dropdown, Form, Space, Switch, Tooltip } from 'antd';
import { MenuProps } from 'antd/lib';
import { FilterSelect } from 'components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions';
import { QueryParams } from 'constants/query';
import { initialQueriesMap } from 'constants/queryBuilder';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import { GripVertical, HardHat, PencilLine } from 'lucide-react';
import { ChevronDown, GripVertical, HardHat, PencilLine } from 'lucide-react';
import { LatencyPointers } from 'pages/TracesFunnelDetails/constants';
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
import { useMemo, useState } from 'react';
import { FunnelStepData } from 'types/api/traceFunnels';
@@ -35,17 +37,16 @@ function FunnelStep({
false,
);
// temporarily hide latency pointer, as it breaks some edge cases (ref: https://signoz-team.slack.com/archives/C089MNX4Y90/p1748600682066499?thread_ts=1748599673.171759&cid=C089MNX4Y90)
// const latencyPointerItems: MenuProps['items'] = LatencyPointers.map(
// (option) => ({
// key: option.value,
// label: option.key,
// style:
// option.value === stepData.latency_pointer
// ? { backgroundColor: 'var(--bg-slate-100)' }
// : {},
// }),
// );
const latencyPointerItems: MenuProps['items'] = LatencyPointers.map(
(option) => ({
key: option.value,
label: option.key,
style:
option.value === stepData.latency_pointer
? { backgroundColor: 'var(--bg-slate-100)' }
: {},
}),
);
const updatedCurrentQuery = useMemo(
() => ({
@@ -175,8 +176,7 @@ function FunnelStep({
/>
<div className="error__label">Errors</div>
</div>
{/* temporarily hide latency pointer, as it breaks some edge cases (ref: https://signoz-team.slack.com/archives/C089MNX4Y90/p1748600682066499?thread_ts=1748599673.171759&cid=C089MNX4Y90) */}
{/* <div className="latency-pointer">
<div className="latency-pointer">
<div className="latency-pointer__label">Latency pointer</div>
<Dropdown
menu={{
@@ -197,7 +197,7 @@ function FunnelStep({
<ChevronDown size={14} color="var(--bg-vanilla-400)" />
</Space>
</Dropdown>
</div> */}
</div>
</div>
</Form>
</div>

View File

@@ -170,10 +170,6 @@ export function FunnelProvider({
handleStepUpdate(index, {
service_name: serviceName,
span_name: spanName,
filters: {
items: [],
op: 'AND',
},
});
logEvent('Trace Funnels: span added (replaced) from trace details page', {});
},
@@ -195,11 +191,6 @@ export function FunnelProvider({
funnelId,
selectedTime,
]);
queryClient.refetchQueries([
REACT_QUERY_KEY.GET_FUNNEL_STEPS_OVERVIEW,
funnelId,
selectedTime,
]);
queryClient.refetchQueries([
REACT_QUERY_KEY.GET_FUNNEL_STEPS_GRAPH_DATA,
funnelId,

View File

@@ -14,7 +14,8 @@ function TracesModulePage(): JSX.Element {
const routes: TabRoutes[] = [
tracesExplorer,
tracesFunnel(pathname),
// TODO(shaheer): remove this check after everything is ready
process.env.NODE_ENV === 'development' ? tracesFunnel(pathname) : null,
tracesSaveView,
].filter(Boolean) as TabRoutes[];

View File

@@ -32,7 +32,7 @@ import { useTranslation } from 'react-i18next';
import { useMutation, useQuery, UseQueryResult } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { useRouteMatch } from 'react-router-dom';
import { Dispatch } from 'redux';
import { Dispatch as ReduxDispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
@@ -51,7 +51,7 @@ const DashboardContext = createContext<IDashboardContext>({
isDashboardSliderOpen: false,
isDashboardLocked: false,
handleToggleDashboardSlider: () => {},
handleDashboardLockToggle: () => {},
handleDashboardLockToggle: async () => {},
dashboardResponse: {} as UseQueryResult<Dashboard, unknown>,
selectedDashboard: {} as Dashboard,
dashboardId: '',
@@ -80,6 +80,13 @@ const DashboardContext = createContext<IDashboardContext>({
isDashboardFetching: false,
columnWidths: {},
setColumnWidths: () => {},
// Global custom data state
globalCustomDataMode: false,
setGlobalCustomDataMode: () => {},
globalCustomXData: 15,
setGlobalCustomXData: () => {},
globalCustomYData: 4,
setGlobalCustomYData: () => {},
});
interface Props {
@@ -146,7 +153,7 @@ export function DashboardProvider({
function setListSortOrder(sortOrder: DashboardSortOrder): void {
if (!isEqual(sortOrder, listSortOrder)) {
setListOrder(sortOrder);
setListOrder(sortOrder as any);
}
params.set('columnKey', sortOrder.columnKey as string);
params.set('order', sortOrder.order as string);
@@ -155,7 +162,7 @@ export function DashboardProvider({
safeNavigate({ search: params.toString() });
}
const dispatch = useDispatch<Dispatch<AppActions>>();
const dispatch = useDispatch<ReduxDispatch<AppActions>>();
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
@@ -416,6 +423,13 @@ export function DashboardProvider({
const [columnWidths, setColumnWidths] = useState<WidgetColumnWidths>({});
// Global custom data state
const [globalCustomDataMode, setGlobalCustomDataMode] = useState<boolean>(
false,
);
const [globalCustomXData, setGlobalCustomXData] = useState<number>(15);
const [globalCustomYData, setGlobalCustomYData] = useState<number>(4);
const value: IDashboardContext = useMemo(
() => ({
toScrollWidgetId,
@@ -424,7 +438,7 @@ export function DashboardProvider({
handleToggleDashboardSlider,
handleDashboardLockToggle,
dashboardResponse,
selectedDashboard,
selectedDashboard: selectedDashboard as Dashboard,
dashboardId,
layouts,
listSortOrder,
@@ -445,6 +459,13 @@ export function DashboardProvider({
isDashboardFetching,
columnWidths,
setColumnWidths,
// Global custom data state
globalCustomDataMode,
setGlobalCustomDataMode,
globalCustomXData,
setGlobalCustomXData,
globalCustomYData,
setGlobalCustomYData,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
@@ -469,6 +490,10 @@ export function DashboardProvider({
isDashboardFetching,
columnWidths,
setColumnWidths,
// Global custom data state
globalCustomDataMode,
globalCustomXData,
globalCustomYData,
],
);

View File

@@ -1,39 +1,40 @@
import dayjs from 'dayjs';
import { Dayjs } from 'dayjs';
import { Dispatch, SetStateAction } from 'react';
import { Layout } from 'react-grid-layout';
import { UseQueryResult } from 'react-query';
import { Dashboard } from 'types/api/dashboard/getAll';
export interface DashboardSortOrder {
columnKey: string;
order: string;
pagination: string;
search: string;
columnKey?: string | null;
order?: string | null;
pagination?: string;
search?: string;
}
export type WidgetColumnWidths = {
[widgetId: string]: Record<string, number>;
};
export interface WidgetColumnWidths {
[key: string]: Record<string, number>;
}
export interface IDashboardContext {
isDashboardSliderOpen: boolean;
isDashboardLocked: boolean;
handleToggleDashboardSlider: (value: boolean) => void;
handleDashboardLockToggle: (value: boolean) => void;
handleDashboardLockToggle: (value: boolean) => Promise<void>;
dashboardResponse: UseQueryResult<Dashboard, unknown>;
selectedDashboard: Dashboard | undefined;
selectedDashboard: Dashboard;
dashboardId: string;
layouts: Layout[];
panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>;
setPanelMap: React.Dispatch<React.SetStateAction<Record<string, any>>>;
listSortOrder: DashboardSortOrder;
setListSortOrder: (sortOrder: DashboardSortOrder) => void;
setLayouts: React.Dispatch<React.SetStateAction<Layout[]>>;
setSelectedDashboard: React.Dispatch<
React.SetStateAction<Dashboard | undefined>
setPanelMap: Dispatch<
SetStateAction<Record<string, { widgets: Layout[]; collapsed: boolean }>>
>;
updatedTimeRef: React.MutableRefObject<dayjs.Dayjs | null>;
listSortOrder: DashboardSortOrder;
setListSortOrder: (value: DashboardSortOrder) => void;
setLayouts: Dispatch<SetStateAction<Layout[]>>;
setSelectedDashboard: Dispatch<SetStateAction<Dashboard | undefined>>;
updatedTimeRef: React.MutableRefObject<Dayjs | null>;
toScrollWidgetId: string;
setToScrollWidgetId: React.Dispatch<React.SetStateAction<string>>;
setToScrollWidgetId: Dispatch<SetStateAction<string>>;
updateLocalStorageDashboardVariables: (
id: string,
selectedValue:
@@ -46,12 +47,19 @@ export interface IDashboardContext {
allSelected: boolean,
) => void;
variablesToGetUpdated: string[];
setVariablesToGetUpdated: React.Dispatch<React.SetStateAction<string[]>>;
setVariablesToGetUpdated: Dispatch<SetStateAction<string[]>>;
dashboardQueryRangeCalled: boolean;
setDashboardQueryRangeCalled: (value: boolean) => void;
setDashboardQueryRangeCalled: Dispatch<SetStateAction<boolean>>;
selectedRowWidgetId: string | null;
setSelectedRowWidgetId: React.Dispatch<React.SetStateAction<string | null>>;
setSelectedRowWidgetId: Dispatch<SetStateAction<string | null>>;
isDashboardFetching: boolean;
columnWidths: WidgetColumnWidths;
setColumnWidths: React.Dispatch<React.SetStateAction<WidgetColumnWidths>>;
setColumnWidths: Dispatch<SetStateAction<WidgetColumnWidths>>;
// Global custom data state
globalCustomDataMode: boolean;
setGlobalCustomDataMode: Dispatch<SetStateAction<boolean>>;
globalCustomXData: number;
setGlobalCustomXData: Dispatch<SetStateAction<number>>;
globalCustomYData: number;
setGlobalCustomYData: Dispatch<SetStateAction<number>>;
}

View File

@@ -118,6 +118,9 @@ export interface IBaseWidget {
columnWidths?: Record<string, number>;
legendPosition?: LegendPosition;
customLegendColors?: Record<string, string>;
customDataMode?: boolean;
customXData?: number; // number of data points
customYData?: number; // number of series
}
export interface Widgets extends IBaseWidget {
query: Query;

View File

@@ -1,789 +0,0 @@
package tracefunnel
import (
"fmt"
)
func BuildTwoStepFunnelValidationQuery(
containsErrorT1 int,
containsErrorT2 int,
startTs int64,
endTs int64,
serviceNameT1 string,
spanNameT1 string,
serviceNameT2 string,
spanNameT2 string,
clauseStep1 string,
clauseStep2 string,
) string {
queryTemplate := `
WITH
%[1]d AS contains_error_t1,
%[2]d AS contains_error_t2,
toDateTime64(%[3]d/1e9, 9) AS start_ts,
toDateTime64(%[4]d/1e9, 9) AS end_ts,
('%[5]s','%[6]s') AS step1,
('%[7]s','%[8]s') AS step2
SELECT
trace_id
FROM (
SELECT
trace_id,
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time
FROM signoz_traces.signoz_index_v3
WHERE
timestamp BETWEEN start_ts AND end_ts
AND (
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[9]s)
OR
(serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[10]s)
)
GROUP BY trace_id
HAVING t1_time > 0 AND t2_time > t1_time
)
ORDER BY t1_time
LIMIT 5;`
return fmt.Sprintf(queryTemplate,
containsErrorT1,
containsErrorT2,
startTs,
endTs,
serviceNameT1,
spanNameT1,
serviceNameT2,
spanNameT2,
clauseStep1,
clauseStep2,
)
}
func BuildThreeStepFunnelValidationQuery(
containsErrorT1 int,
containsErrorT2 int,
containsErrorT3 int,
startTs int64,
endTs int64,
serviceNameT1 string,
spanNameT1 string,
serviceNameT2 string,
spanNameT2 string,
serviceNameT3 string,
spanNameT3 string,
clauseStep1 string,
clauseStep2 string,
clauseStep3 string,
) string {
queryTemplate := `
WITH
%[1]d AS contains_error_t1,
%[2]d AS contains_error_t2,
%[3]d AS contains_error_t3,
toDateTime64(%[4]d/1e9, 9) AS start_ts,
toDateTime64(%[5]d/1e9, 9) AS end_ts,
('%[6]s','%[7]s') AS step1,
('%[8]s','%[9]s') AS step2,
('%[10]s','%[11]s') AS step3
SELECT
trace_id
FROM (
SELECT
trace_id,
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
minIf(timestamp, serviceName = step3.1 AND name = step3.2) AS t3_time
FROM signoz_traces.signoz_index_v3
WHERE
timestamp BETWEEN start_ts AND end_ts
AND (
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[12]s)
OR (serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[13]s)
OR (serviceName = step3.1 AND name = step3.2 AND (contains_error_t3 = 0 OR has_error = true) %[14]s)
)
GROUP BY trace_id
HAVING t1_time > 0 AND t2_time > t1_time AND t3_time > t2_time
)
ORDER BY t1_time
LIMIT 5;`
return fmt.Sprintf(queryTemplate,
containsErrorT1,
containsErrorT2,
containsErrorT3,
startTs,
endTs,
serviceNameT1,
spanNameT1,
serviceNameT2,
spanNameT2,
serviceNameT3,
spanNameT3,
clauseStep1,
clauseStep2,
clauseStep3,
)
}
func BuildTwoStepFunnelOverviewQuery(
containsErrorT1 int,
containsErrorT2 int,
latencyPointerT1 string,
latencyPointerT2 string,
startTs int64,
endTs int64,
serviceNameT1 string,
spanNameT1 string,
serviceNameT2 string,
spanNameT2 string,
clauseStep1 string,
clauseStep2 string,
) string {
queryTemplate := `
WITH
%[1]d AS contains_error_t1,
%[2]d AS contains_error_t2,
'%[3]s' AS latency_pointer_t1,
'%[4]s' AS latency_pointer_t2,
toDateTime64(%[5]d/1e9, 9) AS start_ts,
toDateTime64(%[6]d/1e9, 9) AS end_ts,
(%[6]d - %[5]d)/1e9 AS time_window_sec,
('%[7]s','%[8]s') AS step1,
('%[9]s','%[10]s') AS step2
, funnel AS (
SELECT
trace_id,
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
toUInt8(anyIf(has_error, serviceName = step1.1 AND name = step1.2)) AS s1_error,
toUInt8(anyIf(has_error, serviceName = step2.1 AND name = step2.2)) AS s2_error
FROM signoz_traces.signoz_index_v3
WHERE
timestamp BETWEEN start_ts AND end_ts
AND (
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[11]s)
OR
(serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[12]s)
)
GROUP BY trace_id
HAVING t1_time > 0 AND t2_time > t1_time
)
, totals AS (
SELECT
count() AS total_s1_spans,
countIf(t2_time > t1_time) AS total_s2_spans,
sum(s1_error) AS sum_s1_error,
sum(s2_error) AS sum_s2_error,
avg((toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time)) / 1e6) AS avg_duration,
quantile(0.99)((toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time)) / 1e6) AS latency
FROM funnel
)
SELECT
round(if(total_s1_spans > 0, total_s2_spans * 100.0 / total_s1_spans, 0), 2) AS conversion_rate,
total_s2_spans / time_window_sec AS avg_rate,
greatest(sum_s1_error, sum_s2_error) AS errors,
avg_duration,
latency
FROM totals;
`
return fmt.Sprintf(queryTemplate,
containsErrorT1,
containsErrorT2,
latencyPointerT1,
latencyPointerT2,
startTs,
endTs,
serviceNameT1,
spanNameT1,
serviceNameT2,
spanNameT2,
clauseStep1,
clauseStep2,
)
}
func BuildThreeStepFunnelOverviewQuery(
containsErrorT1 int,
containsErrorT2 int,
containsErrorT3 int,
latencyPointerT1 string,
latencyPointerT2 string,
latencyPointerT3 string,
startTs int64,
endTs int64,
serviceNameT1 string,
spanNameT1 string,
serviceNameT2 string,
spanNameT2 string,
serviceNameT3 string,
spanNameT3 string,
clauseStep1 string,
clauseStep2 string,
clauseStep3 string,
) string {
queryTemplate := `
WITH
%[1]d AS contains_error_t1,
%[2]d AS contains_error_t2,
%[3]d AS contains_error_t3,
'%[4]s' AS latency_pointer_t1,
'%[5]s' AS latency_pointer_t2,
'%[6]s' AS latency_pointer_t3,
toDateTime64(%[7]d/1e9, 9) AS start_ts,
toDateTime64(%[8]d/1e9, 9) AS end_ts,
(%[8]d - %[7]d)/1e9 AS time_window_sec,
('%[9]s','%[10]s') AS step1,
('%[11]s','%[12]s') AS step2,
('%[13]s','%[14]s') AS step3
, funnel AS (
SELECT
trace_id,
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
minIf(timestamp, serviceName = step3.1 AND name = step3.2) AS t3_time,
toUInt8(anyIf(has_error, serviceName = step1.1 AND name = step1.2)) AS s1_error,
toUInt8(anyIf(has_error, serviceName = step2.1 AND name = step2.2)) AS s2_error,
toUInt8(anyIf(has_error, serviceName = step3.1 AND name = step3.2)) AS s3_error
FROM signoz_traces.signoz_index_v3
WHERE
timestamp BETWEEN start_ts AND end_ts
AND (
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[15]s)
OR (serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[16]s)
OR (serviceName = step3.1 AND name = step3.2 AND (contains_error_t3 = 0 OR has_error = true) %[17]s)
)
GROUP BY trace_id
)
, totals AS (
SELECT
countIf(t1_time > 0) AS total_s1_spans,
countIf(t1_time > 0 AND t2_time > t1_time) AS total_s2_spans,
countIf(t2_time > 0 AND t3_time > t2_time) AS total_s3_spans,
sum(s1_error) AS sum_s1_error,
sum(s2_error) AS sum_s2_error,
sum(s3_error) AS sum_s3_error,
avgIf((toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time))/1e6, t1_time > 0 AND t2_time > t1_time) AS avg_duration_12,
quantileIf(0.99)((toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time))/1e6, t1_time > 0 AND t2_time > t1_time) AS latency_12,
avgIf((toUnixTimestamp64Nano(t3_time) - toUnixTimestamp64Nano(t2_time))/1e6, t2_time > 0 AND t3_time > t2_time) AS avg_duration_23,
quantileIf(0.99)((toUnixTimestamp64Nano(t3_time) - toUnixTimestamp64Nano(t2_time))/1e6, t2_time > 0 AND t3_time > t2_time) AS latency_23
FROM funnel
)
SELECT
round(if(total_s1_spans > 0, total_s3_spans * 100.0 / total_s1_spans, 0), 2) AS conversion_rate,
total_s3_spans / nullIf(time_window_sec, 0) AS avg_rate,
greatest(sum_s1_error, sum_s2_error, sum_s3_error) AS errors,
avg_duration_23 AS avg_duration,
latency_23 AS latency
FROM totals;
`
return fmt.Sprintf(
queryTemplate,
containsErrorT1,
containsErrorT2,
containsErrorT3,
latencyPointerT1,
latencyPointerT2,
latencyPointerT3,
startTs,
endTs,
serviceNameT1,
spanNameT1,
serviceNameT2,
spanNameT2,
serviceNameT3,
spanNameT3,
clauseStep1,
clauseStep2,
clauseStep3,
)
}
func BuildTwoStepFunnelCountQuery(
containsErrorT1 int,
containsErrorT2 int,
startTs int64,
endTs int64,
serviceNameT1 string,
spanNameT1 string,
serviceNameT2 string,
spanNameT2 string,
clauseStep1 string,
clauseStep2 string,
) string {
queryTemplate := `
WITH
%[1]d AS contains_error_t1,
%[2]d AS contains_error_t2,
toDateTime64(%[3]d/1e9,9) AS start_ts,
toDateTime64(%[4]d/1e9,9) AS end_ts,
('%[5]s','%[6]s') AS step1,
('%[7]s','%[8]s') AS step2
SELECT
count() AS total_s1_spans,
countIf(t1_error = 1) AS total_s1_errored_spans,
countIf(t2_time > t1_time) AS total_s2_spans,
countIf(t2_time > t1_time AND t2_error = 1) AS total_s2_errored_spans
FROM (
SELECT
trace_id,
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
toUInt8(anyIf(has_error, serviceName = step1.1 AND name = step1.2)) AS t1_error,
toUInt8(anyIf(has_error, serviceName = step2.1 AND name = step2.2)) AS t2_error
FROM signoz_traces.signoz_index_v3
WHERE
timestamp BETWEEN start_ts AND end_ts
AND (
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[9]s)
OR
(serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[10]s)
)
GROUP BY trace_id
HAVING t1_time > 0 AND t2_time > t1_time
) AS funnel;
`
return fmt.Sprintf(queryTemplate,
containsErrorT1,
containsErrorT2,
startTs,
endTs,
serviceNameT1,
spanNameT1,
serviceNameT2,
spanNameT2,
clauseStep1,
clauseStep2,
)
}
func BuildThreeStepFunnelCountQuery(
containsErrorT1 int,
containsErrorT2 int,
containsErrorT3 int,
startTs int64,
endTs int64,
serviceNameT1 string,
spanNameT1 string,
serviceNameT2 string,
spanNameT2 string,
serviceNameT3 string,
spanNameT3 string,
clauseStep1 string,
clauseStep2 string,
clauseStep3 string,
) string {
queryTemplate := `
WITH
%[1]d AS contains_error_t1,
%[2]d AS contains_error_t2,
%[3]d AS contains_error_t3,
toDateTime64(%[4]d/1e9,9) AS start_ts,
toDateTime64(%[5]d/1e9,9) AS end_ts,
('%[6]s','%[7]s') AS step1,
('%[8]s','%[9]s') AS step2,
('%[10]s','%[11]s') AS step3
SELECT
count() AS total_s1_spans,
countIf(t1_error = 1) AS total_s1_errored_spans,
countIf(t2_time > t1_time) AS total_s2_spans,
countIf(t2_time > t1_time AND t2_error = 1) AS total_s2_errored_spans,
countIf(t2_time > t1_time AND t3_time > t2_time) AS total_s3_spans,
countIf(t2_time > t1_time AND t3_time > t2_time AND t3_error = 1) AS total_s3_errored_spans
FROM (
SELECT
trace_id,
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
minIf(timestamp, serviceName = step3.1 AND name = step3.2) AS t3_time,
toUInt8(anyIf(has_error, serviceName = step1.1 AND name = step1.2)) AS t1_error,
toUInt8(anyIf(has_error, serviceName = step2.1 AND name = step2.2)) AS t2_error,
toUInt8(anyIf(has_error, serviceName = step3.1 AND name = step3.2)) AS t3_error
FROM signoz_traces.signoz_index_v3
WHERE
timestamp BETWEEN start_ts AND end_ts
AND (
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[12]s)
OR (serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[13]s)
OR (serviceName = step3.1 AND name = step3.2 AND (contains_error_t3 = 0 OR has_error = true) %[14]s)
)
GROUP BY trace_id
HAVING t1_time > 0 AND t2_time > t1_time AND t3_time > t2_time
) AS funnel;
`
return fmt.Sprintf(queryTemplate,
containsErrorT1,
containsErrorT2,
containsErrorT3,
startTs,
endTs,
serviceNameT1,
spanNameT1,
serviceNameT2,
spanNameT2,
serviceNameT3,
spanNameT3,
clauseStep1,
clauseStep2,
clauseStep3,
)
}
func BuildTwoStepFunnelTopSlowTracesQuery(
containsErrorT1 int,
containsErrorT2 int,
startTs int64,
endTs int64,
serviceNameT1 string,
spanNameT1 string,
serviceNameT2 string,
spanNameT2 string,
clauseStep1 string,
clauseStep2 string,
) string {
queryTemplate := `
WITH
%[1]d AS contains_error_t1,
%[2]d AS contains_error_t2,
toDateTime64(%[3]d/1e9, 9) AS start_ts,
toDateTime64(%[4]d/1e9, 9) AS end_ts,
('%[5]s','%[6]s') AS step1,
('%[7]s','%[8]s') AS step2
SELECT
trace_id,
(toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time)) / 1e6 AS duration_ms,
span_count
FROM (
SELECT
trace_id,
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
count() AS span_count
FROM signoz_traces.signoz_index_v3
WHERE
timestamp BETWEEN start_ts AND end_ts
AND (
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[9]s)
OR
(serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[10]s)
)
GROUP BY trace_id
HAVING t1_time > 0 AND t2_time > t1_time
) AS funnel
ORDER BY duration_ms DESC
LIMIT 5;
`
return fmt.Sprintf(queryTemplate,
containsErrorT1,
containsErrorT2,
startTs,
endTs,
serviceNameT1,
spanNameT1,
serviceNameT2,
spanNameT2,
clauseStep1,
clauseStep2,
)
}
func BuildTwoStepFunnelTopSlowErrorTracesQuery(
containsErrorT1 int,
containsErrorT2 int,
startTs int64,
endTs int64,
serviceNameT1 string,
spanNameT1 string,
serviceNameT2 string,
spanNameT2 string,
clauseStep1 string,
clauseStep2 string,
) string {
queryTemplate := `
WITH
%[1]d AS contains_error_t1,
%[2]d AS contains_error_t2,
toDateTime64(%[3]d/1e9, 9) AS start_ts,
toDateTime64(%[4]d/1e9, 9) AS end_ts,
('%[5]s','%[6]s') AS step1,
('%[7]s','%[8]s') AS step2
SELECT
trace_id,
(toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time)) / 1e6 AS duration_ms,
span_count
FROM (
SELECT
trace_id,
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
toUInt8(anyIf(has_error, serviceName = step1.1 AND name = step1.2)) AS t1_error,
toUInt8(anyIf(has_error, serviceName = step2.1 AND name = step2.2)) AS t2_error,
count() AS span_count
FROM signoz_traces.signoz_index_v3
WHERE
timestamp BETWEEN start_ts AND end_ts
AND (
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[9]s)
OR
(serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[10]s)
)
GROUP BY trace_id
HAVING t1_time > 0 AND t2_time > t1_time
) AS funnel
WHERE
(t1_error = 1 OR t2_error = 1)
ORDER BY duration_ms DESC
LIMIT 5;
`
return fmt.Sprintf(queryTemplate,
containsErrorT1,
containsErrorT2,
startTs,
endTs,
serviceNameT1,
spanNameT1,
serviceNameT2,
spanNameT2,
clauseStep1,
clauseStep2,
)
}
func BuildTwoStepFunnelStepOverviewQuery(
containsErrorT1 int,
containsErrorT2 int,
latencyPointerT1 string,
latencyPointerT2 string,
startTs int64,
endTs int64,
serviceNameT1 string,
spanNameT1 string,
serviceNameT2 string,
spanNameT2 string,
clauseStep1 string,
clauseStep2 string,
latencyTypeT2 string,
) string {
const tpl = `
WITH
toDateTime64(%[5]d / 1e9, 9) AS start_ts,
toDateTime64(%[6]d / 1e9, 9) AS end_ts,
(%[6]d - %[5]d) / 1e9 AS time_window_sec,
('%[7]s', '%[8]s') AS step1,
('%[9]s', '%[10]s') AS step2,
%[1]d AS contains_error_t1,
%[2]d AS contains_error_t2
SELECT
round(total_s2_spans * 100.0 / total_s1_spans, 2) AS conversion_rate,
total_s2_spans / time_window_sec AS avg_rate,
greatest(sum_s1_error, sum_s2_error) AS errors,
avg_duration,
latency
FROM (
SELECT
countIf(t1_time > 0) AS total_s1_spans,
countIf(t1_time > 0 AND t2_time > t1_time) AS total_s2_spans,
sum(s1_error) AS sum_s1_error,
sum(s2_error) AS sum_s2_error,
avgIf(
(toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time)) / 1e6,
t1_time > 0 AND t2_time > t1_time
) AS avg_duration,
quantileIf(%[13]s)(
(toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time)) / 1e6,
t1_time > 0 AND t2_time > t1_time
) AS latency
FROM (
SELECT
trace_id,
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
toUInt8(anyIf(has_error, serviceName = step1.1 AND name = step1.2)) AS s1_error,
toUInt8(anyIf(has_error, serviceName = step2.1 AND name = step2.2)) AS s2_error
FROM signoz_traces.signoz_index_v3
WHERE
timestamp BETWEEN start_ts AND end_ts
AND serviceName IN (step1.1, step2.1)
AND name IN (step1.2, step2.2)
AND ((contains_error_t1 = 0) OR (has_error AND serviceName = step1.1 AND name = step1.2)) %[11]s
AND ((contains_error_t2 = 0) OR (has_error AND serviceName = step2.1 AND name = step2.2)) %[12]s
GROUP BY trace_id
) AS funnel
) AS totals;
`
return fmt.Sprintf(tpl,
containsErrorT1,
containsErrorT2,
latencyPointerT1,
latencyPointerT2,
startTs,
endTs,
serviceNameT1,
spanNameT1,
serviceNameT2,
spanNameT2,
clauseStep1,
clauseStep2,
latencyTypeT2,
)
}
func BuildThreeStepFunnelStepOverviewQuery(
containsErrorT1 int,
containsErrorT2 int,
containsErrorT3 int,
latencyPointerT1 string,
latencyPointerT2 string,
latencyPointerT3 string,
startTs int64,
endTs int64,
serviceNameT1 string,
spanNameT1 string,
serviceNameT2 string,
spanNameT2 string,
serviceNameT3 string,
spanNameT3 string,
clauseStep1 string,
clauseStep2 string,
clauseStep3 string,
stepStart int64,
stepEnd int64,
latencyTypeT2 string,
latencyTypeT3 string,
) string {
const baseWithAndFunnel = `
WITH
toDateTime64(%[7]d/1e9, 9) AS start_ts,
toDateTime64(%[8]d/1e9, 9) AS end_ts,
(%[8]d - %[7]d) / 1e9 AS time_window_sec,
('%[9]s','%[10]s') AS step1,
('%[11]s','%[12]s') AS step2,
('%[13]s','%[14]s') AS step3,
%[1]d AS contains_error_t1,
%[2]d AS contains_error_t2,
%[3]d AS contains_error_t3,
funnel AS (
SELECT
trace_id,
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
minIf(timestamp, serviceName = step3.1 AND name = step3.2) AS t3_time,
toUInt8(anyIf(has_error, serviceName = step1.1 AND name = step1.2)) AS s1_error,
toUInt8(anyIf(has_error, serviceName = step2.1 AND name = step2.2)) AS s2_error,
toUInt8(anyIf(has_error, serviceName = step3.1 AND name = step3.2)) AS s3_error
FROM signoz_traces.signoz_index_v3
WHERE
timestamp BETWEEN start_ts AND end_ts
AND serviceName IN (step1.1, step2.1, step3.1)
AND name IN (step1.2, step2.2, step3.2)
AND ((contains_error_t1 = 0) OR (has_error AND serviceName = step1.1 AND name = step1.2)) %[15]s
AND ((contains_error_t2 = 0) OR (has_error AND serviceName = step2.1 AND name = step2.2)) %[16]s
AND ((contains_error_t3 = 0) OR (has_error AND serviceName = step3.1 AND name = step3.2)) %[17]s
GROUP BY trace_id
)
`
const totals12 = `
SELECT
round(if(total_s1_spans > 0, total_s2_spans * 100.0 / total_s1_spans, 0), 2) AS conversion_rate,
total_s2_spans / time_window_sec AS avg_rate,
greatest(sum_s1_error, sum_s2_error) AS errors,
avg_duration_12 AS avg_duration,
latency_12 AS latency
FROM (
SELECT
countIf(t1_time > 0 AND t2_time > t1_time) AS total_s2_spans,
countIf(t1_time > 0) AS total_s1_spans, -- eligible only
sum(s1_error) AS sum_s1_error,
sum(s2_error) AS sum_s2_error,
avgIf((toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time)) / 1e6, t1_time > 0 AND t2_time > t1_time) AS avg_duration_12,
quantileIf(%[18]s)((toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time)) / 1e6, t1_time > 0 AND t2_time > t1_time) AS latency_12
FROM funnel
) AS totals;
`
const totals23 = `
SELECT
round(if(total_s2_spans > 0, total_s3_spans * 100.0 / total_s2_spans, 0), 2) AS conversion_rate,
total_s3_spans / time_window_sec AS avg_rate,
greatest(sum_s2_error, sum_s3_error) AS errors,
avg_duration_23 AS avg_duration,
latency_23 AS latency
FROM (
SELECT
countIf(t2_time > 0 AND t3_time > t2_time) AS total_s3_spans,
countIf(t2_time > 0) AS total_s2_spans, -- eligible only
sum(s2_error) AS sum_s2_error,
sum(s3_error) AS sum_s3_error,
avgIf((toUnixTimestamp64Nano(t3_time) - toUnixTimestamp64Nano(t2_time)) / 1e6, t2_time > 0 AND t3_time > t2_time) AS avg_duration_23,
quantileIf(%[19]s)((toUnixTimestamp64Nano(t3_time) - toUnixTimestamp64Nano(t2_time)) / 1e6, t2_time > 0 AND t3_time > t2_time) AS latency_23
FROM funnel
) AS totals;
`
const fallback = `
SELECT 0 AS conversion_rate, 0 AS avg_rate, 0 AS errors, 0 AS avg_duration, 0 AS latency;
`
var totalsTpl string
switch {
case stepStart == 1 && stepEnd == 2:
totalsTpl = totals12
case stepStart == 2 && stepEnd == 3:
totalsTpl = totals23
default:
totalsTpl = fallback
}
return fmt.Sprintf(
baseWithAndFunnel+totalsTpl,
containsErrorT1,
containsErrorT2,
containsErrorT3,
latencyPointerT1,
latencyPointerT2,
latencyPointerT3,
startTs,
endTs,
serviceNameT1,
spanNameT1,
serviceNameT2,
spanNameT2,
serviceNameT3,
spanNameT3,
clauseStep1,
clauseStep2,
clauseStep3,
latencyTypeT2,
latencyTypeT3,
)
}

View File

@@ -28,7 +28,7 @@ func (store *store) Create(ctx context.Context, funnel *traceFunnels.StorableFun
Where("name = ? AND org_id = ?", funnel.Name, funnel.OrgID.String()).
Exists(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to check for existing funnel")
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to check for existing funnelr")
}
if exists {
return store.sqlstore.WrapAlreadyExistsErrf(nil, traceFunnels.ErrFunnelAlreadyExists, "a funnel with name '%s' already exists in this organization", funnel.Name)

View File

@@ -1,471 +0,0 @@
package tracefunnel
import (
"fmt"
"strings"
tracev4 "github.com/SigNoz/signoz/pkg/query-service/app/traces/v4"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
)
// sanitizeClause adds AND prefix to non-empty clauses
func sanitizeClause(clause string) string {
if clause == "" {
return ""
}
return "AND " + clause
}
func ValidateTraces(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange) (*v3.ClickHouseQuery, error) {
var query string
var err error
funnelSteps := funnel.Steps
containsErrorT1 := 0
containsErrorT2 := 0
containsErrorT3 := 0
if funnelSteps[0].HasErrors {
containsErrorT1 = 1
}
if funnelSteps[1].HasErrors {
containsErrorT2 = 1
}
if len(funnel.Steps) > 2 && funnelSteps[2].HasErrors {
containsErrorT3 = 1
}
// Build filter clauses for each step
clauseStep1, err := tracev4.BuildTracesFilter(funnelSteps[0].Filters)
if err != nil {
return nil, err
}
clauseStep2, err := tracev4.BuildTracesFilter(funnelSteps[1].Filters)
if err != nil {
return nil, err
}
clauseStep3 := ""
if len(funnel.Steps) > 2 {
clauseStep3, err = tracev4.BuildTracesFilter(funnelSteps[2].Filters)
if err != nil {
return nil, err
}
}
// Sanitize clauses
clauseStep1 = sanitizeClause(clauseStep1)
clauseStep2 = sanitizeClause(clauseStep2)
clauseStep3 = sanitizeClause(clauseStep3)
if len(funnel.Steps) > 2 {
query = BuildThreeStepFunnelValidationQuery(
containsErrorT1,
containsErrorT2,
containsErrorT3,
timeRange.StartTime,
timeRange.EndTime,
funnelSteps[0].ServiceName,
funnelSteps[0].SpanName,
funnelSteps[1].ServiceName,
funnelSteps[1].SpanName,
funnelSteps[2].ServiceName,
funnelSteps[2].SpanName,
clauseStep1,
clauseStep2,
clauseStep3,
)
} else {
query = BuildTwoStepFunnelValidationQuery(
containsErrorT1,
containsErrorT2,
timeRange.StartTime,
timeRange.EndTime,
funnelSteps[0].ServiceName,
funnelSteps[0].SpanName,
funnelSteps[1].ServiceName,
funnelSteps[1].SpanName,
clauseStep1,
clauseStep2,
)
}
return &v3.ClickHouseQuery{
Query: query,
}, nil
}
func GetFunnelAnalytics(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange) (*v3.ClickHouseQuery, error) {
var query string
var err error
funnelSteps := funnel.Steps
containsErrorT1 := 0
containsErrorT2 := 0
containsErrorT3 := 0
latencyPointerT1 := funnelSteps[0].LatencyPointer
latencyPointerT2 := funnelSteps[1].LatencyPointer
latencyPointerT3 := "start"
if len(funnel.Steps) > 2 {
latencyPointerT3 = funnelSteps[2].LatencyPointer
}
if funnelSteps[0].HasErrors {
containsErrorT1 = 1
}
if funnelSteps[1].HasErrors {
containsErrorT2 = 1
}
if len(funnel.Steps) > 2 && funnelSteps[2].HasErrors {
containsErrorT3 = 1
}
// Build filter clauses for each step
clauseStep1, err := tracev4.BuildTracesFilter(funnelSteps[0].Filters)
if err != nil {
return nil, err
}
clauseStep2, err := tracev4.BuildTracesFilter(funnelSteps[1].Filters)
if err != nil {
return nil, err
}
clauseStep3 := ""
if len(funnel.Steps) > 2 {
clauseStep3, err = tracev4.BuildTracesFilter(funnelSteps[2].Filters)
if err != nil {
return nil, err
}
}
// Sanitize clauses
clauseStep1 = sanitizeClause(clauseStep1)
clauseStep2 = sanitizeClause(clauseStep2)
clauseStep3 = sanitizeClause(clauseStep3)
if len(funnel.Steps) > 2 {
query = BuildThreeStepFunnelOverviewQuery(
containsErrorT1,
containsErrorT2,
containsErrorT3,
latencyPointerT1,
latencyPointerT2,
latencyPointerT3,
timeRange.StartTime,
timeRange.EndTime,
funnelSteps[0].ServiceName,
funnelSteps[0].SpanName,
funnelSteps[1].ServiceName,
funnelSteps[1].SpanName,
funnelSteps[2].ServiceName,
funnelSteps[2].SpanName,
clauseStep1,
clauseStep2,
clauseStep3,
)
} else {
query = BuildTwoStepFunnelOverviewQuery(
containsErrorT1,
containsErrorT2,
latencyPointerT1,
latencyPointerT2,
timeRange.StartTime,
timeRange.EndTime,
funnelSteps[0].ServiceName,
funnelSteps[0].SpanName,
funnelSteps[1].ServiceName,
funnelSteps[1].SpanName,
clauseStep1,
clauseStep2,
)
}
return &v3.ClickHouseQuery{Query: query}, nil
}
func GetFunnelStepAnalytics(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange, stepStart, stepEnd int64) (*v3.ClickHouseQuery, error) {
var query string
var err error
funnelSteps := funnel.Steps
containsErrorT1 := 0
containsErrorT2 := 0
containsErrorT3 := 0
latencyPointerT1 := funnelSteps[0].LatencyPointer
latencyPointerT2 := funnelSteps[1].LatencyPointer
latencyPointerT3 := "start"
if len(funnel.Steps) > 2 {
latencyPointerT3 = funnelSteps[2].LatencyPointer
}
latencyTypeT2 := "0.99"
latencyTypeT3 := "0.99"
if stepStart == stepEnd {
return nil, fmt.Errorf("step start and end cannot be the same for /step/overview")
}
if funnelSteps[0].HasErrors {
containsErrorT1 = 1
}
if funnelSteps[1].HasErrors {
containsErrorT2 = 1
}
if len(funnel.Steps) > 2 && funnelSteps[2].HasErrors {
containsErrorT3 = 1
}
if funnelSteps[1].LatencyType != "" {
latency := strings.ToLower(funnelSteps[1].LatencyType)
if latency == "p90" {
latencyTypeT2 = "0.90"
} else if latency == "p95" {
latencyTypeT2 = "0.95"
} else {
latencyTypeT2 = "0.99"
}
}
if len(funnel.Steps) > 2 && funnelSteps[2].LatencyType != "" {
latency := strings.ToLower(funnelSteps[2].LatencyType)
if latency == "p90" {
latencyTypeT3 = "0.90"
} else if latency == "p95" {
latencyTypeT3 = "0.95"
} else {
latencyTypeT3 = "0.99"
}
}
// Build filter clauses for each step
clauseStep1, err := tracev4.BuildTracesFilter(funnelSteps[0].Filters)
if err != nil {
return nil, err
}
clauseStep2, err := tracev4.BuildTracesFilter(funnelSteps[1].Filters)
if err != nil {
return nil, err
}
clauseStep3 := ""
if len(funnel.Steps) > 2 {
clauseStep3, err = tracev4.BuildTracesFilter(funnelSteps[2].Filters)
if err != nil {
return nil, err
}
}
// Sanitize clauses
clauseStep1 = sanitizeClause(clauseStep1)
clauseStep2 = sanitizeClause(clauseStep2)
clauseStep3 = sanitizeClause(clauseStep3)
if len(funnel.Steps) > 2 {
query = BuildThreeStepFunnelStepOverviewQuery(
containsErrorT1,
containsErrorT2,
containsErrorT3,
latencyPointerT1,
latencyPointerT2,
latencyPointerT3,
timeRange.StartTime,
timeRange.EndTime,
funnelSteps[0].ServiceName,
funnelSteps[0].SpanName,
funnelSteps[1].ServiceName,
funnelSteps[1].SpanName,
funnelSteps[2].ServiceName,
funnelSteps[2].SpanName,
clauseStep1,
clauseStep2,
clauseStep3,
stepStart,
stepEnd,
latencyTypeT2,
latencyTypeT3,
)
} else {
query = BuildTwoStepFunnelStepOverviewQuery(
containsErrorT1,
containsErrorT2,
latencyPointerT1,
latencyPointerT2,
timeRange.StartTime,
timeRange.EndTime,
funnelSteps[0].ServiceName,
funnelSteps[0].SpanName,
funnelSteps[1].ServiceName,
funnelSteps[1].SpanName,
clauseStep1,
clauseStep2,
latencyTypeT2,
)
}
return &v3.ClickHouseQuery{Query: query}, nil
}
func GetStepAnalytics(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange) (*v3.ClickHouseQuery, error) {
var query string
funnelSteps := funnel.Steps
containsErrorT1 := 0
containsErrorT2 := 0
containsErrorT3 := 0
if funnelSteps[0].HasErrors {
containsErrorT1 = 1
}
if funnelSteps[1].HasErrors {
containsErrorT2 = 1
}
if len(funnel.Steps) > 2 && funnelSteps[2].HasErrors {
containsErrorT3 = 1
}
// Build filter clauses for each step
clauseStep1, err := tracev4.BuildTracesFilter(funnelSteps[0].Filters)
if err != nil {
return nil, err
}
clauseStep2, err := tracev4.BuildTracesFilter(funnelSteps[1].Filters)
if err != nil {
return nil, err
}
clauseStep3 := ""
if len(funnel.Steps) > 2 {
clauseStep3, err = tracev4.BuildTracesFilter(funnelSteps[2].Filters)
if err != nil {
return nil, err
}
}
// Sanitize clauses
clauseStep1 = sanitizeClause(clauseStep1)
clauseStep2 = sanitizeClause(clauseStep2)
clauseStep3 = sanitizeClause(clauseStep3)
if len(funnel.Steps) > 2 {
query = BuildThreeStepFunnelCountQuery(
containsErrorT1,
containsErrorT2,
containsErrorT3,
timeRange.StartTime,
timeRange.EndTime,
funnelSteps[0].ServiceName,
funnelSteps[0].SpanName,
funnelSteps[1].ServiceName,
funnelSteps[1].SpanName,
funnelSteps[2].ServiceName,
funnelSteps[2].SpanName,
clauseStep1,
clauseStep2,
clauseStep3,
)
} else {
query = BuildTwoStepFunnelCountQuery(
containsErrorT1,
containsErrorT2,
timeRange.StartTime,
timeRange.EndTime,
funnelSteps[0].ServiceName,
funnelSteps[0].SpanName,
funnelSteps[1].ServiceName,
funnelSteps[1].SpanName,
clauseStep1,
clauseStep2,
)
}
return &v3.ClickHouseQuery{
Query: query,
}, nil
}
func GetSlowestTraces(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange, stepStart, stepEnd int64) (*v3.ClickHouseQuery, error) {
funnelSteps := funnel.Steps
containsErrorT1 := 0
containsErrorT2 := 0
stepStartOrder := 0
stepEndOrder := 1
if stepStart != stepEnd {
stepStartOrder = int(stepStart) - 1
stepEndOrder = int(stepEnd) - 1
if funnelSteps[stepStartOrder].HasErrors {
containsErrorT1 = 1
}
if funnelSteps[stepEndOrder].HasErrors {
containsErrorT2 = 1
}
}
// Build filter clauses for the steps
clauseStep1, err := tracev4.BuildTracesFilter(funnelSteps[stepStartOrder].Filters)
if err != nil {
return nil, err
}
clauseStep2, err := tracev4.BuildTracesFilter(funnelSteps[stepEndOrder].Filters)
if err != nil {
return nil, err
}
// Sanitize clauses
clauseStep1 = sanitizeClause(clauseStep1)
clauseStep2 = sanitizeClause(clauseStep2)
query := BuildTwoStepFunnelTopSlowTracesQuery(
containsErrorT1,
containsErrorT2,
timeRange.StartTime,
timeRange.EndTime,
funnelSteps[stepStartOrder].ServiceName,
funnelSteps[stepStartOrder].SpanName,
funnelSteps[stepEndOrder].ServiceName,
funnelSteps[stepEndOrder].SpanName,
clauseStep1,
clauseStep2,
)
return &v3.ClickHouseQuery{Query: query}, nil
}
func GetErroredTraces(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange, stepStart, stepEnd int64) (*v3.ClickHouseQuery, error) {
funnelSteps := funnel.Steps
containsErrorT1 := 0
containsErrorT2 := 0
stepStartOrder := 0
stepEndOrder := 1
if stepStart != stepEnd {
stepStartOrder = int(stepStart) - 1
stepEndOrder = int(stepEnd) - 1
if funnelSteps[stepStartOrder].HasErrors {
containsErrorT1 = 1
}
if funnelSteps[stepEndOrder].HasErrors {
containsErrorT2 = 1
}
}
// Build filter clauses for the steps
clauseStep1, err := tracev4.BuildTracesFilter(funnelSteps[stepStartOrder].Filters)
if err != nil {
return nil, err
}
clauseStep2, err := tracev4.BuildTracesFilter(funnelSteps[stepEndOrder].Filters)
if err != nil {
return nil, err
}
// Sanitize clauses
clauseStep1 = sanitizeClause(clauseStep1)
clauseStep2 = sanitizeClause(clauseStep2)
query := BuildTwoStepFunnelTopSlowErrorTracesQuery(
containsErrorT1,
containsErrorT2,
timeRange.StartTime,
timeRange.EndTime,
funnelSteps[stepStartOrder].ServiceName,
funnelSteps[stepStartOrder].SpanName,
funnelSteps[stepEndOrder].ServiceName,
funnelSteps[stepEndOrder].SpanName,
clauseStep1,
clauseStep2,
)
return &v3.ClickHouseQuery{Query: query}, nil
}

View File

@@ -40,7 +40,6 @@ import (
_ "github.com/mattn/go-sqlite3"
"github.com/SigNoz/signoz/pkg/cache"
traceFunnelsModule "github.com/SigNoz/signoz/pkg/modules/tracefunnel"
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/inframetrics"
@@ -64,7 +63,6 @@ import (
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
"go.uber.org/zap"
@@ -5239,258 +5237,22 @@ func (aH *APIHandler) RegisterTraceFunnelsRoutes(router *mux.Router, am *middlew
// API endpoints
traceFunnelsRouter.HandleFunc("/new",
am.ViewAccess(aH.Signoz.Handlers.TraceFunnel.New)).
am.EditAccess(aH.Signoz.Handlers.TraceFunnel.New)).
Methods(http.MethodPost)
traceFunnelsRouter.HandleFunc("/list",
am.ViewAccess(aH.Signoz.Handlers.TraceFunnel.List)).
Methods(http.MethodGet)
traceFunnelsRouter.HandleFunc("/steps/update",
am.ViewAccess(aH.Signoz.Handlers.TraceFunnel.UpdateSteps)).
am.EditAccess(aH.Signoz.Handlers.TraceFunnel.UpdateSteps)).
Methods(http.MethodPut)
traceFunnelsRouter.HandleFunc("/{funnel_id}",
am.ViewAccess(aH.Signoz.Handlers.TraceFunnel.Get)).
Methods(http.MethodGet)
traceFunnelsRouter.HandleFunc("/{funnel_id}",
am.ViewAccess(aH.Signoz.Handlers.TraceFunnel.Delete)).
am.EditAccess(aH.Signoz.Handlers.TraceFunnel.Delete)).
Methods(http.MethodDelete)
traceFunnelsRouter.HandleFunc("/{funnel_id}",
am.ViewAccess(aH.Signoz.Handlers.TraceFunnel.UpdateFunnel)).
am.EditAccess(aH.Signoz.Handlers.TraceFunnel.UpdateFunnel)).
Methods(http.MethodPut)
// Analytics endpoints
traceFunnelsRouter.HandleFunc("/{funnel_id}/analytics/validate", aH.handleValidateTraces).Methods("POST")
traceFunnelsRouter.HandleFunc("/{funnel_id}/analytics/overview", aH.handleFunnelAnalytics).Methods("POST")
traceFunnelsRouter.HandleFunc("/{funnel_id}/analytics/steps", aH.handleStepAnalytics).Methods("POST")
traceFunnelsRouter.HandleFunc("/{funnel_id}/analytics/steps/overview", aH.handleFunnelStepAnalytics).Methods("POST")
traceFunnelsRouter.HandleFunc("/{funnel_id}/analytics/slow-traces", aH.handleFunnelSlowTraces).Methods("POST")
traceFunnelsRouter.HandleFunc("/{funnel_id}/analytics/error-traces", aH.handleFunnelErrorTraces).Methods("POST")
}
func (aH *APIHandler) handleValidateTraces(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
funnelID := vars["funnel_id"]
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
funnel, err := aH.Signoz.Modules.TraceFunnel.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("funnel not found: %v", err)}, nil)
return
}
var timeRange traceFunnels.TimeRange
if err := json.NewDecoder(r.Body).Decode(&timeRange); err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding time range: %v", err)}, nil)
return
}
if len(funnel.Steps) < 2 {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("funnel must have at least 2 steps")}, nil)
return
}
chq, err := traceFunnelsModule.ValidateTraces(funnel, timeRange)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building clickhouse query: %v", err)}, nil)
return
}
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting clickhouse results to list: %v", err)}, nil)
return
}
aH.Respond(w, results)
}
func (aH *APIHandler) handleFunnelAnalytics(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
funnelID := vars["funnel_id"]
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
funnel, err := aH.Signoz.Modules.TraceFunnel.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("funnel not found: %v", err)}, nil)
return
}
var stepTransition traceFunnels.StepTransitionRequest
if err := json.NewDecoder(r.Body).Decode(&stepTransition); err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding time range: %v", err)}, nil)
return
}
chq, err := traceFunnelsModule.GetFunnelAnalytics(funnel, stepTransition.TimeRange)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building clickhouse query: %v", err)}, nil)
return
}
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting clickhouse results to list: %v", err)}, nil)
return
}
aH.Respond(w, results)
}
func (aH *APIHandler) handleFunnelStepAnalytics(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
funnelID := vars["funnel_id"]
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
funnel, err := aH.Signoz.Modules.TraceFunnel.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("funnel not found: %v", err)}, nil)
return
}
var stepTransition traceFunnels.StepTransitionRequest
if err := json.NewDecoder(r.Body).Decode(&stepTransition); err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding time range: %v", err)}, nil)
return
}
chq, err := traceFunnelsModule.GetFunnelStepAnalytics(funnel, stepTransition.TimeRange, stepTransition.StepStart, stepTransition.StepEnd)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building clickhouse query: %v", err)}, nil)
return
}
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting clickhouse results to list: %v", err)}, nil)
return
}
aH.Respond(w, results)
}
func (aH *APIHandler) handleStepAnalytics(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
funnelID := vars["funnel_id"]
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
funnel, err := aH.Signoz.Modules.TraceFunnel.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("funnel not found: %v", err)}, nil)
return
}
var timeRange traceFunnels.TimeRange
if err := json.NewDecoder(r.Body).Decode(&timeRange); err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding time range: %v", err)}, nil)
return
}
chq, err := traceFunnelsModule.GetStepAnalytics(funnel, timeRange)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building clickhouse query: %v", err)}, nil)
return
}
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting clickhouse results to list: %v", err)}, nil)
return
}
aH.Respond(w, results)
}
func (aH *APIHandler) handleFunnelSlowTraces(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
funnelID := vars["funnel_id"]
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
funnel, err := aH.Signoz.Modules.TraceFunnel.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("funnel not found: %v", err)}, nil)
return
}
var req traceFunnels.StepTransitionRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("invalid request body: %v", err)}, nil)
return
}
chq, err := traceFunnelsModule.GetSlowestTraces(funnel, req.TimeRange, req.StepStart, req.StepEnd)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building clickhouse query: %v", err)}, nil)
return
}
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting clickhouse results to list: %v", err)}, nil)
return
}
aH.Respond(w, results)
}
func (aH *APIHandler) handleFunnelErrorTraces(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
funnelID := vars["funnel_id"]
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, err)
return
}
funnel, err := aH.Signoz.Modules.TraceFunnel.Get(r.Context(), valuer.MustNewUUID(funnelID), valuer.MustNewUUID(claims.OrgID))
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("funnel not found: %v", err)}, nil)
return
}
var req traceFunnels.StepTransitionRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("invalid request body: %v", err)}, nil)
return
}
chq, err := traceFunnelsModule.GetErroredTraces(funnel, req.TimeRange, req.StepStart, req.StepEnd)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error building clickhouse query: %v", err)}, nil)
return
}
results, err := aH.reader.GetListResultV3(r.Context(), chq.Query)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error converting clickhouse results to list: %v", err)}, nil)
return
}
aH.Respond(w, results)
}

View File

@@ -87,62 +87,6 @@ func existsSubQueryForFixedColumn(key v3.AttributeKey, op v3.FilterOperator) (st
}
}
func BuildTracesFilter(fs *v3.FilterSet) (string, error) {
var conditions []string
if fs != nil && len(fs.Items) != 0 {
for _, item := range fs.Items {
val := item.Value
// generate the key
columnName := getColumnName(item.Key)
var fmtVal string
item.Operator = v3.FilterOperator(strings.ToLower(strings.TrimSpace(string(item.Operator))))
if item.Operator != v3.FilterOperatorExists && item.Operator != v3.FilterOperatorNotExists {
var err error
val, err = utils.ValidateAndCastValue(val, item.Key.DataType)
if err != nil {
return "", fmt.Errorf("invalid value for key %s: %v", item.Key.Key, err)
}
}
if val != nil {
fmtVal = utils.ClickHouseFormattedValue(val)
}
if operator, ok := tracesOperatorMappingV3[item.Operator]; ok {
switch item.Operator {
case v3.FilterOperatorContains, v3.FilterOperatorNotContains:
// we also want to treat %, _ as literals for contains
val := utils.QuoteEscapedStringForContains(fmt.Sprintf("%s", item.Value), false)
conditions = append(conditions, fmt.Sprintf("%s %s '%%%s%%'", columnName, operator, val))
case v3.FilterOperatorRegex, v3.FilterOperatorNotRegex:
conditions = append(conditions, fmt.Sprintf(operator, columnName, fmtVal))
case v3.FilterOperatorExists, v3.FilterOperatorNotExists:
if item.Key.IsColumn {
subQuery, err := existsSubQueryForFixedColumn(item.Key, item.Operator)
if err != nil {
return "", err
}
conditions = append(conditions, subQuery)
} else {
cType := getClickHouseTracesColumnType(item.Key.Type)
cDataType := getClickHouseTracesColumnDataType(item.Key.DataType)
col := fmt.Sprintf("%s_%s", cType, cDataType)
conditions = append(conditions, fmt.Sprintf(operator, col, item.Key.Key))
}
default:
conditions = append(conditions, fmt.Sprintf("%s %s %s", columnName, operator, fmtVal))
}
} else {
return "", fmt.Errorf("unsupported operator %s", item.Operator)
}
}
}
queryString := strings.Join(conditions, " AND ")
return queryString, nil
}
func BuildTracesFilterQuery(fs *v3.FilterSet) (string, error) {
var conditions []string