mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-20 10:20:48 +01:00
Compare commits
57 Commits
SIG-5761
...
chore/tf/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ddf002d46 | ||
|
|
31cd192f24 | ||
|
|
3aacb99adc | ||
|
|
40ca9adbfc | ||
|
|
bfa7a06e90 | ||
|
|
189046865a | ||
|
|
d5ee6ca2c3 | ||
|
|
62fb05ac5a | ||
|
|
5a3ed26f01 | ||
|
|
8e490e4089 | ||
|
|
d3adc319ad | ||
|
|
6b58e859b5 | ||
|
|
3d3a1eaaf2 | ||
|
|
361640fd22 | ||
|
|
89de68f987 | ||
|
|
71b098776b | ||
|
|
6b3e2759a1 | ||
|
|
ea46639f59 | ||
|
|
69ed1b7d02 | ||
|
|
5b07705157 | ||
|
|
2fc8bb4585 | ||
|
|
11d75940e8 | ||
|
|
3caaa51e08 | ||
|
|
cad3bf6883 | ||
|
|
5b7ce41d0d | ||
|
|
ee5120d4ed | ||
|
|
c249620e8f | ||
|
|
1c1811b216 | ||
|
|
86d69f74f3 | ||
|
|
37b26a7116 | ||
|
|
26ad89ed70 | ||
|
|
94c7512a6a | ||
|
|
333ff86a6b | ||
|
|
a22d061ec1 | ||
|
|
22fdeb1381 | ||
|
|
77b330cfe9 | ||
|
|
78a5d7e39e | ||
|
|
ea1f4e8253 | ||
|
|
5e0d6110b5 | ||
|
|
14ce7f80e2 | ||
|
|
f8341e8958 | ||
|
|
7a7428d73e | ||
|
|
77cd490e48 | ||
|
|
9a96817a88 | ||
|
|
da9a2520a4 | ||
|
|
c03bf9905c | ||
|
|
6676832c71 | ||
|
|
03e50d3bc3 | ||
|
|
95bc3987bb | ||
|
|
eb797edc53 | ||
|
|
c2d36480a2 | ||
|
|
ab0d9918b2 | ||
|
|
c364a3e695 | ||
|
|
43337b6697 | ||
|
|
e258d70df5 | ||
|
|
19ee5860cb | ||
|
|
235ea39d73 |
@@ -167,8 +167,8 @@ interface UpdateFunnelDescriptionPayload {
|
||||
export const saveFunnelDescription = async (
|
||||
payload: UpdateFunnelDescriptionPayload,
|
||||
): Promise<SuccessResponse<FunnelData> | ErrorResponse> => {
|
||||
const response: AxiosResponse = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/save`,
|
||||
const response: AxiosResponse = await axios.put(
|
||||
`${FUNNELS_BASE_PATH}/${payload.funnel_id}`,
|
||||
payload,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
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;
|
||||
@@ -1,91 +0,0 @@
|
||||
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;
|
||||
@@ -1,88 +0,0 @@
|
||||
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;
|
||||
@@ -121,10 +121,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.global-custom-data-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dashboard-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -12,7 +12,6 @@ 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';
|
||||
@@ -479,9 +478,6 @@ 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) => (
|
||||
|
||||
@@ -12,7 +12,6 @@ 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, {
|
||||
@@ -114,12 +113,6 @@ function RightContainer({
|
||||
customLegendColors,
|
||||
setCustomLegendColors,
|
||||
queryResponse,
|
||||
customDataMode,
|
||||
setCustomDataMode,
|
||||
customXData,
|
||||
setCustomXData,
|
||||
customYData,
|
||||
setCustomYData,
|
||||
}: RightContainerProps): JSX.Element {
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const [inputValue, setInputValue] = useState(title);
|
||||
@@ -287,30 +280,6 @@ 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
|
||||
@@ -585,12 +554,6 @@ 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 = {
|
||||
|
||||
@@ -239,17 +239,6 @@ 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) {
|
||||
@@ -279,9 +268,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
legendPosition,
|
||||
customLegendColors,
|
||||
columnWidths: columnWidths?.[selectedWidget?.id],
|
||||
customDataMode,
|
||||
customXData,
|
||||
customYData,
|
||||
};
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -308,9 +294,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
legendPosition,
|
||||
customLegendColors,
|
||||
columnWidths,
|
||||
customDataMode,
|
||||
customXData,
|
||||
customYData,
|
||||
]);
|
||||
|
||||
const closeModal = (): void => {
|
||||
@@ -520,9 +503,6 @@ 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,
|
||||
},
|
||||
]
|
||||
: [
|
||||
@@ -552,9 +532,6 @@ 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,
|
||||
],
|
||||
@@ -819,12 +796,6 @@ 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>
|
||||
|
||||
@@ -10,19 +10,12 @@ 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';
|
||||
@@ -42,14 +35,7 @@ function UplotPanelWrapper({
|
||||
customTooltipElement,
|
||||
customSeries,
|
||||
}: PanelWrapperProps): JSX.Element {
|
||||
const {
|
||||
toScrollWidgetId,
|
||||
setToScrollWidgetId,
|
||||
globalCustomDataMode,
|
||||
globalCustomXData,
|
||||
globalCustomYData,
|
||||
} = useDashboard();
|
||||
|
||||
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const lineChartRef = useRef<ToggleGraphProps>();
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
@@ -57,11 +43,6 @@ 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(() => {
|
||||
@@ -75,64 +56,12 @@ 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(effectiveQueryResponse);
|
||||
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||
|
||||
setMinTimeScale(startTime);
|
||||
setMaxTimeScale(endTime);
|
||||
}, [effectiveQueryResponse]);
|
||||
}, [queryResponse]);
|
||||
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
|
||||
@@ -140,71 +69,28 @@ function UplotPanelWrapper({
|
||||
const {
|
||||
graphVisibilityStates: localStoredVisibilityState,
|
||||
} = getLocalStorageGraphVisibilityState({
|
||||
apiResponse: effectiveQueryResponse.data?.payload.data.result || [],
|
||||
apiResponse: queryResponse.data?.payload.data.result || [],
|
||||
name: widget.id,
|
||||
});
|
||||
if (setGraphVisibility) {
|
||||
setGraphVisibility(localStoredVisibilityState);
|
||||
}
|
||||
}, [
|
||||
effectiveQueryResponse.data?.payload.data.result,
|
||||
setGraphVisibility,
|
||||
widget.id,
|
||||
]);
|
||||
}, [queryResponse.data?.payload.data.result, setGraphVisibility, widget.id]);
|
||||
|
||||
if (effectiveQueryResponse.data && widget.panelTypes === PANEL_TYPES.BAR) {
|
||||
if (queryResponse.data && widget.panelTypes === PANEL_TYPES.BAR) {
|
||||
const sortedSeriesData = getSortedSeriesData(
|
||||
effectiveQueryResponse.data?.payload.data.result,
|
||||
queryResponse.data?.payload.data.result,
|
||||
);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
effectiveQueryResponse.data.payload.data.result = sortedSeriesData;
|
||||
queryResponse.data.payload.data.result = sortedSeriesData;
|
||||
}
|
||||
|
||||
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,
|
||||
const chartData = getUPlotChartData(
|
||||
queryResponse?.data?.payload,
|
||||
widget.fillSpans,
|
||||
widget?.stackedBarChart,
|
||||
hiddenGraph,
|
||||
]);
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (widget.panelTypes === PANEL_TYPES.BAR && widget?.stackedBarChart) {
|
||||
@@ -224,22 +110,16 @@ 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: effectiveQueryResponse.data?.payload,
|
||||
apiResponse: queryResponse.data?.payload,
|
||||
dimensions: containerDimensions,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
yAxisUnit: widget?.yAxisUnit,
|
||||
onClickHandler: enhancedClickHandler,
|
||||
onClickHandler: onClickHandler || _noop,
|
||||
thresholds: widget.thresholds,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
@@ -270,11 +150,11 @@ function UplotPanelWrapper({
|
||||
widget.softMin,
|
||||
widget.panelTypes,
|
||||
widget?.stackedBarChart,
|
||||
effectiveQueryResponse.data?.payload,
|
||||
queryResponse.data?.payload,
|
||||
containerDimensions,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
enhancedClickHandler,
|
||||
onClickHandler,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
graphVisibility,
|
||||
@@ -291,14 +171,6 @@ 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} />
|
||||
@@ -311,10 +183,7 @@ function UplotPanelWrapper({
|
||||
)}
|
||||
{isFullViewMode && setGraphVisibility && !widget?.stackedBarChart && (
|
||||
<GraphManager
|
||||
data={getUPlotChartData(
|
||||
effectiveQueryResponse?.data?.payload,
|
||||
widget.fillSpans,
|
||||
)}
|
||||
data={getUPlotChartData(queryResponse?.data?.payload, widget.fillSpans)}
|
||||
name={widget.id}
|
||||
options={options}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
|
||||
@@ -149,30 +149,28 @@ function SpanOverview({
|
||||
<Typography.Text className="service-name">
|
||||
{span.serviceName}
|
||||
</Typography.Text>
|
||||
{!!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>
|
||||
)}
|
||||
{!!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>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -450,7 +448,7 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
virtualiserRef={virtualizerRef}
|
||||
setColumnWidths={setTraceFlamegraphStatsWidth}
|
||||
/>
|
||||
{selectedSpanToAddToFunnel && process.env.NODE_ENV === 'development' && (
|
||||
{selectedSpanToAddToFunnel && (
|
||||
<AddSpanToFunnelModal
|
||||
span={selectedSpanToAddToFunnel}
|
||||
isOpen={isAddSpanToFunnelModalOpen}
|
||||
|
||||
@@ -142,6 +142,7 @@ export const useValidateFunnelSteps = ({
|
||||
interface SaveFunnelDescriptionPayload {
|
||||
funnel_id: string;
|
||||
description: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export const useSaveFunnelDescription = (): UseMutationResult<
|
||||
@@ -149,7 +150,11 @@ export const useSaveFunnelDescription = (): UseMutationResult<
|
||||
Error,
|
||||
SaveFunnelDescriptionPayload
|
||||
> =>
|
||||
useMutation({
|
||||
useMutation<
|
||||
SuccessResponse<FunnelData> | ErrorResponse,
|
||||
Error,
|
||||
SaveFunnelDescriptionPayload
|
||||
>({
|
||||
mutationFn: saveFunnelDescription,
|
||||
});
|
||||
|
||||
|
||||
@@ -431,10 +431,9 @@ 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();
|
||||
}
|
||||
|
||||
@@ -67,19 +67,15 @@ export default function TraceDetailsPage(): JSX.Element {
|
||||
key: 'trace-details',
|
||||
children: <TraceDetailsV2 />,
|
||||
},
|
||||
...(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">
|
||||
<Cone className="funnel-icon" size={16} /> Funnels
|
||||
</div>
|
||||
),
|
||||
key: 'funnels',
|
||||
children: <div />,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="tab-item">
|
||||
|
||||
@@ -41,6 +41,7 @@ function AddFunnelDescriptionModal({
|
||||
{
|
||||
funnel_id: funnelId,
|
||||
description,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import './FunnelStep.styles.scss';
|
||||
|
||||
import { Button, Divider, Dropdown, Form, Space, Switch, Tooltip } from 'antd';
|
||||
import { MenuProps } from 'antd/lib';
|
||||
import { Button, Divider, Form, Switch, Tooltip } from 'antd';
|
||||
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 { ChevronDown, GripVertical, HardHat, PencilLine } from 'lucide-react';
|
||||
import { LatencyPointers } from 'pages/TracesFunnelDetails/constants';
|
||||
import { GripVertical, HardHat, PencilLine } from 'lucide-react';
|
||||
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { FunnelStepData } from 'types/api/traceFunnels';
|
||||
@@ -37,16 +35,17 @@ function FunnelStep({
|
||||
false,
|
||||
);
|
||||
|
||||
const latencyPointerItems: MenuProps['items'] = LatencyPointers.map(
|
||||
(option) => ({
|
||||
key: option.value,
|
||||
label: option.key,
|
||||
style:
|
||||
option.value === stepData.latency_pointer
|
||||
? { backgroundColor: 'var(--bg-slate-100)' }
|
||||
: {},
|
||||
}),
|
||||
);
|
||||
// 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 updatedCurrentQuery = useMemo(
|
||||
() => ({
|
||||
@@ -176,7 +175,8 @@ function FunnelStep({
|
||||
/>
|
||||
<div className="error__label">Errors</div>
|
||||
</div>
|
||||
<div className="latency-pointer">
|
||||
{/* 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__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>
|
||||
|
||||
@@ -170,6 +170,10 @@ 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', {});
|
||||
},
|
||||
@@ -191,6 +195,11 @@ 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,
|
||||
|
||||
@@ -14,8 +14,7 @@ function TracesModulePage(): JSX.Element {
|
||||
|
||||
const routes: TabRoutes[] = [
|
||||
tracesExplorer,
|
||||
// TODO(shaheer): remove this check after everything is ready
|
||||
process.env.NODE_ENV === 'development' ? tracesFunnel(pathname) : null,
|
||||
tracesFunnel(pathname),
|
||||
tracesSaveView,
|
||||
].filter(Boolean) as TabRoutes[];
|
||||
|
||||
|
||||
@@ -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 as ReduxDispatch } from 'redux';
|
||||
import { Dispatch } 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: async () => {},
|
||||
handleDashboardLockToggle: () => {},
|
||||
dashboardResponse: {} as UseQueryResult<Dashboard, unknown>,
|
||||
selectedDashboard: {} as Dashboard,
|
||||
dashboardId: '',
|
||||
@@ -80,13 +80,6 @@ const DashboardContext = createContext<IDashboardContext>({
|
||||
isDashboardFetching: false,
|
||||
columnWidths: {},
|
||||
setColumnWidths: () => {},
|
||||
// Global custom data state
|
||||
globalCustomDataMode: false,
|
||||
setGlobalCustomDataMode: () => {},
|
||||
globalCustomXData: 15,
|
||||
setGlobalCustomXData: () => {},
|
||||
globalCustomYData: 4,
|
||||
setGlobalCustomYData: () => {},
|
||||
});
|
||||
|
||||
interface Props {
|
||||
@@ -153,7 +146,7 @@ export function DashboardProvider({
|
||||
|
||||
function setListSortOrder(sortOrder: DashboardSortOrder): void {
|
||||
if (!isEqual(sortOrder, listSortOrder)) {
|
||||
setListOrder(sortOrder as any);
|
||||
setListOrder(sortOrder);
|
||||
}
|
||||
params.set('columnKey', sortOrder.columnKey as string);
|
||||
params.set('order', sortOrder.order as string);
|
||||
@@ -162,7 +155,7 @@ export function DashboardProvider({
|
||||
safeNavigate({ search: params.toString() });
|
||||
}
|
||||
|
||||
const dispatch = useDispatch<ReduxDispatch<AppActions>>();
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
@@ -423,13 +416,6 @@ 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,
|
||||
@@ -438,7 +424,7 @@ export function DashboardProvider({
|
||||
handleToggleDashboardSlider,
|
||||
handleDashboardLockToggle,
|
||||
dashboardResponse,
|
||||
selectedDashboard: selectedDashboard as Dashboard,
|
||||
selectedDashboard,
|
||||
dashboardId,
|
||||
layouts,
|
||||
listSortOrder,
|
||||
@@ -459,13 +445,6 @@ export function DashboardProvider({
|
||||
isDashboardFetching,
|
||||
columnWidths,
|
||||
setColumnWidths,
|
||||
// Global custom data state
|
||||
globalCustomDataMode,
|
||||
setGlobalCustomDataMode,
|
||||
globalCustomXData,
|
||||
setGlobalCustomXData,
|
||||
globalCustomYData,
|
||||
setGlobalCustomYData,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
@@ -490,10 +469,6 @@ export function DashboardProvider({
|
||||
isDashboardFetching,
|
||||
columnWidths,
|
||||
setColumnWidths,
|
||||
// Global custom data state
|
||||
globalCustomDataMode,
|
||||
globalCustomXData,
|
||||
globalCustomYData,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,40 +1,39 @@
|
||||
import { Dayjs } from 'dayjs';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
export interface DashboardSortOrder {
|
||||
columnKey?: string | null;
|
||||
order?: string | null;
|
||||
pagination?: string;
|
||||
search?: string;
|
||||
columnKey: string;
|
||||
order: string;
|
||||
pagination: string;
|
||||
search: string;
|
||||
}
|
||||
|
||||
export interface WidgetColumnWidths {
|
||||
[key: string]: Record<string, number>;
|
||||
}
|
||||
export type WidgetColumnWidths = {
|
||||
[widgetId: string]: Record<string, number>;
|
||||
};
|
||||
|
||||
export interface IDashboardContext {
|
||||
isDashboardSliderOpen: boolean;
|
||||
isDashboardLocked: boolean;
|
||||
handleToggleDashboardSlider: (value: boolean) => void;
|
||||
handleDashboardLockToggle: (value: boolean) => Promise<void>;
|
||||
handleDashboardLockToggle: (value: boolean) => void;
|
||||
dashboardResponse: UseQueryResult<Dashboard, unknown>;
|
||||
selectedDashboard: Dashboard;
|
||||
selectedDashboard: Dashboard | undefined;
|
||||
dashboardId: string;
|
||||
layouts: Layout[];
|
||||
panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>;
|
||||
setPanelMap: Dispatch<
|
||||
SetStateAction<Record<string, { widgets: Layout[]; collapsed: boolean }>>
|
||||
>;
|
||||
setPanelMap: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
||||
listSortOrder: DashboardSortOrder;
|
||||
setListSortOrder: (value: DashboardSortOrder) => void;
|
||||
setLayouts: Dispatch<SetStateAction<Layout[]>>;
|
||||
setSelectedDashboard: Dispatch<SetStateAction<Dashboard | undefined>>;
|
||||
updatedTimeRef: React.MutableRefObject<Dayjs | null>;
|
||||
setListSortOrder: (sortOrder: DashboardSortOrder) => void;
|
||||
setLayouts: React.Dispatch<React.SetStateAction<Layout[]>>;
|
||||
setSelectedDashboard: React.Dispatch<
|
||||
React.SetStateAction<Dashboard | undefined>
|
||||
>;
|
||||
updatedTimeRef: React.MutableRefObject<dayjs.Dayjs | null>;
|
||||
toScrollWidgetId: string;
|
||||
setToScrollWidgetId: Dispatch<SetStateAction<string>>;
|
||||
setToScrollWidgetId: React.Dispatch<React.SetStateAction<string>>;
|
||||
updateLocalStorageDashboardVariables: (
|
||||
id: string,
|
||||
selectedValue:
|
||||
@@ -47,19 +46,12 @@ export interface IDashboardContext {
|
||||
allSelected: boolean,
|
||||
) => void;
|
||||
variablesToGetUpdated: string[];
|
||||
setVariablesToGetUpdated: Dispatch<SetStateAction<string[]>>;
|
||||
setVariablesToGetUpdated: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
dashboardQueryRangeCalled: boolean;
|
||||
setDashboardQueryRangeCalled: Dispatch<SetStateAction<boolean>>;
|
||||
setDashboardQueryRangeCalled: (value: boolean) => void;
|
||||
selectedRowWidgetId: string | null;
|
||||
setSelectedRowWidgetId: Dispatch<SetStateAction<string | null>>;
|
||||
setSelectedRowWidgetId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
isDashboardFetching: boolean;
|
||||
columnWidths: 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>>;
|
||||
setColumnWidths: React.Dispatch<React.SetStateAction<WidgetColumnWidths>>;
|
||||
}
|
||||
|
||||
@@ -118,9 +118,6 @@ 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;
|
||||
|
||||
789
pkg/modules/tracefunnel/clickhouse_queries.go
Normal file
789
pkg/modules/tracefunnel/clickhouse_queries.go
Normal file
@@ -0,0 +1,789 @@
|
||||
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,
|
||||
)
|
||||
}
|
||||
@@ -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 funnelr")
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to check for existing funnel")
|
||||
}
|
||||
if exists {
|
||||
return store.sqlstore.WrapAlreadyExistsErrf(nil, traceFunnels.ErrFunnelAlreadyExists, "a funnel with name '%s' already exists in this organization", funnel.Name)
|
||||
|
||||
471
pkg/modules/tracefunnel/query.go
Normal file
471
pkg/modules/tracefunnel/query.go
Normal file
@@ -0,0 +1,471 @@
|
||||
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
|
||||
}
|
||||
@@ -40,6 +40,7 @@ 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"
|
||||
@@ -63,6 +64,7 @@ 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"
|
||||
|
||||
@@ -5237,22 +5239,258 @@ func (aH *APIHandler) RegisterTraceFunnelsRoutes(router *mux.Router, am *middlew
|
||||
|
||||
// API endpoints
|
||||
traceFunnelsRouter.HandleFunc("/new",
|
||||
am.EditAccess(aH.Signoz.Handlers.TraceFunnel.New)).
|
||||
am.ViewAccess(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.EditAccess(aH.Signoz.Handlers.TraceFunnel.UpdateSteps)).
|
||||
am.ViewAccess(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.EditAccess(aH.Signoz.Handlers.TraceFunnel.Delete)).
|
||||
am.ViewAccess(aH.Signoz.Handlers.TraceFunnel.Delete)).
|
||||
Methods(http.MethodDelete)
|
||||
traceFunnelsRouter.HandleFunc("/{funnel_id}",
|
||||
am.EditAccess(aH.Signoz.Handlers.TraceFunnel.UpdateFunnel)).
|
||||
am.ViewAccess(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)
|
||||
}
|
||||
|
||||
@@ -87,6 +87,62 @@ 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user