diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx
index 58345e51b9e..abe5f861007 100644
--- a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx
+++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx
@@ -149,30 +149,28 @@ function SpanOverview({
{span.serviceName}
- {!!span.serviceName &&
- !!span.name &&
- process.env.NODE_ENV === 'development' && (
-
-
·
-
- )}
+ {!!span.serviceName && !!span.name && (
+
+
·
+
+ )}
@@ -475,7 +473,7 @@ function Success(props: ISuccessProps): JSX.Element {
virtualiserRef={virtualizerRef}
setColumnWidths={setTraceFlamegraphStatsWidth}
/>
- {selectedSpanToAddToFunnel && process.env.NODE_ENV === 'development' && (
+ {selectedSpanToAddToFunnel && (
,
},
- ...(process.env.NODE_ENV === 'development'
- ? [
- {
- label: (
-
- Funnels
-
- ),
- key: 'funnels',
- children: ,
- },
- ]
- : []),
+ {
+ label: (
+
+ Funnels
+
+ ),
+ key: 'funnels',
+ children: ,
+ },
{
label: (
diff --git a/frontend/src/pages/TracesModulePage/TracesModulePage.tsx b/frontend/src/pages/TracesModulePage/TracesModulePage.tsx
index 790c8b964b3..a235284d228 100644
--- a/frontend/src/pages/TracesModulePage/TracesModulePage.tsx
+++ b/frontend/src/pages/TracesModulePage/TracesModulePage.tsx
@@ -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[];
diff --git a/pkg/modules/tracefunnel/clickhouse_queries.go b/pkg/modules/tracefunnel/clickhouse_queries.go
new file mode 100644
index 00000000000..362a6364fa7
--- /dev/null
+++ b/pkg/modules/tracefunnel/clickhouse_queries.go
@@ -0,0 +1,790 @@
+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(DISTINCT trace_id) AS total_s1_spans,
+ count(DISTINCT CASE WHEN t2_time > t1_time THEN trace_id END) AS total_s2_spans,
+ count(DISTINCT CASE WHEN s1_error = 1 THEN trace_id END) AS sum_s1_error,
+ count(DISTINCT CASE WHEN s2_error = 1 THEN trace_id END) 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
+ count(DISTINCT trace_id) AS total_s1_spans,
+ count(DISTINCT CASE WHEN t2_time > t1_time THEN trace_id END) AS total_s2_spans,
+ count(DISTINCT CASE WHEN t3_time > t2_time THEN trace_id END) AS total_s3_spans,
+
+ count(DISTINCT CASE WHEN s1_error = 1 THEN trace_id END) AS sum_s1_error,
+ count(DISTINCT CASE WHEN s2_error = 1 THEN trace_id END) AS sum_s2_error,
+ count(DISTINCT CASE WHEN s3_error = 1 THEN trace_id END) 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(DISTINCT trace_id) AS total_s1_spans,
+ count(DISTINCT CASE WHEN t1_error = 1 THEN trace_id END) AS total_s1_errored_spans,
+ count(DISTINCT CASE WHEN t2_time > t1_time THEN trace_id END) AS total_s2_spans,
+ count(DISTINCT CASE WHEN t2_time > t1_time AND t2_error = 1 THEN trace_id END) 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(DISTINCT trace_id) AS total_s1_spans,
+ count(DISTINCT CASE WHEN t1_error = 1 THEN trace_id END) AS total_s1_errored_spans,
+ count(DISTINCT CASE WHEN t2_time > t1_time THEN trace_id END) AS total_s2_spans,
+ count(DISTINCT CASE WHEN t2_time > t1_time AND t2_error = 1 THEN trace_id END) AS total_s2_errored_spans,
+ count(DISTINCT CASE WHEN t2_time > t1_time AND t3_time > t2_time THEN trace_id END) AS total_s3_spans,
+ count(DISTINCT CASE WHEN t2_time > t1_time AND t3_time > t2_time AND t3_error = 1 THEN trace_id END) 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
+ count(DISTINCT trace_id) AS total_s1_spans,
+ count(DISTINCT CASE WHEN t2_time > t1_time THEN trace_id END) AS total_s2_spans,
+ count(DISTINCT CASE WHEN s1_error = 1 THEN trace_id END) AS sum_s1_error,
+ count(DISTINCT CASE WHEN s2_error = 1 THEN trace_id END) 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 = 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
+ ) 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 = 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
+ )
+`
+
+ 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
+ count(DISTINCT CASE WHEN t2_time > t1_time THEN trace_id END) AS total_s2_spans,
+ count(DISTINCT trace_id) AS total_s1_spans,
+ count(DISTINCT CASE WHEN s1_error = 1 THEN trace_id END) AS sum_s1_error,
+ count(DISTINCT CASE WHEN s2_error = 1 THEN trace_id END) 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
+ count(DISTINCT CASE WHEN t2_time > 0 AND t3_time > t2_time THEN trace_id END) AS total_s3_spans,
+ count(DISTINCT CASE WHEN t2_time > 0 THEN trace_id END) AS total_s2_spans,
+ count(DISTINCT CASE WHEN s2_error = 1 THEN trace_id END) AS sum_s2_error,
+ count(DISTINCT CASE WHEN s3_error = 1 THEN trace_id END) 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,
+ )
+}
diff --git a/pkg/modules/tracefunnel/query.go b/pkg/modules/tracefunnel/query.go
new file mode 100644
index 00000000000..0ec98b6faae
--- /dev/null
+++ b/pkg/modules/tracefunnel/query.go
@@ -0,0 +1,475 @@
+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 if not already present
+func sanitizeClause(clause string) string {
+ if clause == "" {
+ return ""
+ }
+ // Check if clause already starts with AND
+ if strings.HasPrefix(strings.TrimSpace(clause), "AND") {
+ return clause
+ }
+ 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
+}
diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go
index 63711c8f7cd..f3428c1e943 100644
--- a/pkg/query-service/app/http_handler.go
+++ b/pkg/query-service/app/http_handler.go
@@ -20,8 +20,6 @@ import (
"text/template"
"time"
- "github.com/SigNoz/signoz/pkg/query-service/constants"
-
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apis/fields"
errorsV2 "github.com/SigNoz/signoz/pkg/errors"
@@ -41,6 +39,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"
@@ -56,6 +55,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder"
tracesV3 "github.com/SigNoz/signoz/pkg/query-service/app/traces/v3"
tracesV4 "github.com/SigNoz/signoz/pkg/query-service/app/traces/v4"
+ "github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/contextlinks"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/query-service/postprocess"
@@ -65,6 +65,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"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"
@@ -5216,4 +5217,421 @@ func (aH *APIHandler) RegisterTraceFunnelsRoutes(router *mux.Router, am *middlew
traceFunnelsRouter.HandleFunc("/{funnel_id}",
am.EditAccess(aH.Signoz.Handlers.TraceFunnel.UpdateFunnel)).
Methods(http.MethodPut)
+
+ // Analytics endpoints
+ traceFunnelsRouter.HandleFunc("/{funnel_id}/analytics/validate", aH.handleValidateTraces).Methods("POST")
+ traceFunnelsRouter.HandleFunc("/{funnel_id}/analytics/overview", aH.handleFunnelAnalytics).Methods("POST")
+ traceFunnelsRouter.HandleFunc("/{funnel_id}/analytics/steps", aH.handleStepAnalytics).Methods("POST")
+ traceFunnelsRouter.HandleFunc("/{funnel_id}/analytics/steps/overview", aH.handleFunnelStepAnalytics).Methods("POST")
+ traceFunnelsRouter.HandleFunc("/{funnel_id}/analytics/slow-traces", aH.handleFunnelSlowTraces).Methods("POST")
+ traceFunnelsRouter.HandleFunc("/{funnel_id}/analytics/error-traces", aH.handleFunnelErrorTraces).Methods("POST")
+
+ // Analytics endpoints
+ traceFunnelsRouter.HandleFunc("/analytics/validate", aH.handleValidateTracesWithPayload).Methods("POST")
+ traceFunnelsRouter.HandleFunc("/analytics/overview", aH.handleFunnelAnalyticsWithPayload).Methods("POST")
+ traceFunnelsRouter.HandleFunc("/analytics/steps", aH.handleStepAnalyticsWithPayload).Methods("POST")
+ traceFunnelsRouter.HandleFunc("/analytics/steps/overview", aH.handleFunnelStepAnalyticsWithPayload).Methods("POST")
+ traceFunnelsRouter.HandleFunc("/analytics/slow-traces", aH.handleFunnelSlowTracesWithPayload).Methods("POST")
+ traceFunnelsRouter.HandleFunc("/analytics/error-traces", aH.handleFunnelErrorTracesWithPayload).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)
+}
+
+func (aH *APIHandler) handleValidateTracesWithPayload(w http.ResponseWriter, r *http.Request) {
+ var req traceFunnels.PostableFunnel
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding request: %v", err)}, nil)
+ return
+ }
+
+ if len(req.Steps) < 2 {
+ RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("funnel must have at least 2 steps")}, nil)
+ return
+ }
+
+ // Create a StorableFunnel from the request
+ funnel := &traceFunnels.StorableFunnel{
+ Steps: req.Steps,
+ }
+
+ chq, err := traceFunnelsModule.ValidateTraces(funnel, traceFunnels.TimeRange{
+ StartTime: req.StartTime,
+ EndTime: req.EndTime,
+ })
+ 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) handleFunnelAnalyticsWithPayload(w http.ResponseWriter, r *http.Request) {
+ var req traceFunnels.PostableFunnel
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding request: %v", err)}, nil)
+ return
+ }
+
+ funnel := &traceFunnels.StorableFunnel{
+ Steps: req.Steps,
+ }
+
+ chq, err := traceFunnelsModule.GetFunnelAnalytics(funnel, traceFunnels.TimeRange{
+ StartTime: req.StartTime,
+ EndTime: req.EndTime,
+ })
+ 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) handleStepAnalyticsWithPayload(w http.ResponseWriter, r *http.Request) {
+ var req traceFunnels.PostableFunnel
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding request: %v", err)}, nil)
+ return
+ }
+
+ funnel := &traceFunnels.StorableFunnel{
+ Steps: req.Steps,
+ }
+
+ chq, err := traceFunnelsModule.GetStepAnalytics(funnel, traceFunnels.TimeRange{
+ StartTime: req.StartTime,
+ EndTime: req.EndTime,
+ })
+ 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) handleFunnelStepAnalyticsWithPayload(w http.ResponseWriter, r *http.Request) {
+ var req traceFunnels.PostableFunnel
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding request: %v", err)}, nil)
+ return
+ }
+
+ funnel := &traceFunnels.StorableFunnel{
+ Steps: req.Steps,
+ }
+
+ chq, err := traceFunnelsModule.GetFunnelStepAnalytics(funnel, traceFunnels.TimeRange{
+ StartTime: req.StartTime,
+ EndTime: req.EndTime,
+ }, 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) handleFunnelSlowTracesWithPayload(w http.ResponseWriter, r *http.Request) {
+ var req traceFunnels.PostableFunnel
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding request: %v", err)}, nil)
+ return
+ }
+
+ funnel := &traceFunnels.StorableFunnel{
+ Steps: req.Steps,
+ }
+
+ chq, err := traceFunnelsModule.GetSlowestTraces(funnel, traceFunnels.TimeRange{
+ StartTime: req.StartTime,
+ EndTime: req.EndTime,
+ }, 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) handleFunnelErrorTracesWithPayload(w http.ResponseWriter, r *http.Request) {
+ var req traceFunnels.PostableFunnel
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error decoding request: %v", err)}, nil)
+ return
+ }
+
+ funnel := &traceFunnels.StorableFunnel{
+ Steps: req.Steps,
+ }
+
+ chq, err := traceFunnelsModule.GetErroredTraces(funnel, traceFunnels.TimeRange{
+ StartTime: req.StartTime,
+ EndTime: req.EndTime,
+ }, 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)
}
diff --git a/pkg/query-service/app/traces/v4/query_builder.go b/pkg/query-service/app/traces/v4/query_builder.go
index 69a76de4d18..38a6bd85708 100644
--- a/pkg/query-service/app/traces/v4/query_builder.go
+++ b/pkg/query-service/app/traces/v4/query_builder.go
@@ -148,6 +148,59 @@ func BuildTracesFilterQuery(fs *v3.FilterSet) (string, error) {
return queryString, nil
}
+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)
+ }
+ }
+ }
+ return strings.Join(conditions, " AND "), nil
+}
+
func handleEmptyValuesInGroupBy(groupBy []v3.AttributeKey) (string, error) {
// TODO(nitya): in future when we support user based mat column handle them
// skipping now as we don't support creating them
diff --git a/pkg/types/tracefunneltypes/tracefunnel.go b/pkg/types/tracefunneltypes/tracefunnel.go
index fdc51d943ad..4eb725e3794 100644
--- a/pkg/types/tracefunneltypes/tracefunnel.go
+++ b/pkg/types/tracefunneltypes/tracefunnel.go
@@ -49,10 +49,10 @@ type PostableFunnel struct {
UserID string `json:"user_id,omitempty"`
// Analytics specific fields
- StartTime int64 `json:"start_time,omitempty"`
- EndTime int64 `json:"end_time,omitempty"`
- StepAOrder int64 `json:"step_a_order,omitempty"`
- StepBOrder int64 `json:"step_b_order,omitempty"`
+ StartTime int64 `json:"start_time,omitempty"`
+ EndTime int64 `json:"end_time,omitempty"`
+ StepStart int64 `json:"step_start,omitempty"`
+ StepEnd int64 `json:"step_end,omitempty"`
}
// GettableFunnel represents all possible funnel-related responses