Ankit Nayan f2abddd2ed
feat: refactor tracefunnel to support dynamic multi-step funnels (#8627)
* feat: refactor tracefunnel to support dynamic multi-step funnels

Replace hardcoded 2-step and 3-step funnel functions with dynamic
implementations that support unlimited steps. Add comprehensive tests
for multi-step funnel functionality while maintaining backward
compatibility.

Key changes:
- Add dynamic query builders for n-step funnels
- Update all query functions to use new builders
- Remove old hardcoded functions
- Add tests for 1-6 step funnels
- Maintain temporal ordering logic

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: add duration calculation for latency_pointer='end' in funnel qu… (#8632)

* feat: add duration calculation for latency_pointer='end' in funnel queries

- Updated BuildFunnelOverviewQuery and BuildFunnelStepOverviewQuery to calculate end time
  when latency_pointer is 'end'
- Modified BuildFunnelTopSlowTracesQuery and BuildFunnelTopSlowErrorTracesQuery to support
  latency pointer parameters
- Added comprehensive tests for latency pointer functionality in
  clickhouse_queries_latency_test.go
- When latency_pointer is 'end', the query now adds span duration to timestamp for
  accurate latency calculations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* do matching after lowercase conversion

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: Ankit Nayan <ankitnayan@Ankits-MacBook-Pro.local>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* fix: apply remaining changes from PR #8615 for ClickHouse 25.5 compatibility

- Updated BuildTracesFilter to BuildTracesFilterQuery with false parameter in query.go
- Updated test files to expect resource_string_service$$name instead of serviceName
- Fixed function reference in query_test.go

These changes complete the ClickHouse 25.5 compatibility updates while maintaining
the dynamic multi-step funnel functionality.

* fix: replace durationNano with duration_nano for ClickHouse compatibility

- Updated all SQL queries in clickhouse_queries.go to use duration_nano column name
- Updated test expectations in clickhouse_queries_latency_test.go
- Ensures consistency with ClickHouse snake_case column naming convention

* refactor: code formatting and add TODO comment

- Remove trailing whitespace in query.go
- Add TODO comment for GetErroredTraces function regarding product improvement
- Add newline at end of file for proper formatting

---------

Co-authored-by: Ankit Nayan <ankitnayan@Ankits-MacBook-Pro.local>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-29 16:18:15 +00:00

329 lines
8.4 KiB
Go

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) {
funnelSteps := funnel.Steps
// Build step data for the dynamic query builder
steps := make([]struct {
ServiceName string
SpanName string
ContainsError int
Clause string
}, len(funnelSteps))
for i, step := range funnelSteps {
// Build filter clause
clause, err := tracev4.BuildTracesFilterQuery(step.Filters, false)
if err != nil {
return nil, err
}
steps[i] = struct {
ServiceName string
SpanName string
ContainsError int
Clause string
}{
ServiceName: step.ServiceName,
SpanName: step.SpanName,
ContainsError: 0,
Clause: sanitizeClause(clause),
}
if step.HasErrors {
steps[i].ContainsError = 1
}
}
query := BuildFunnelValidationQuery(steps, timeRange.StartTime, timeRange.EndTime)
return &v3.ClickHouseQuery{
Query: query,
}, nil
}
func GetFunnelAnalytics(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange) (*v3.ClickHouseQuery, error) {
funnelSteps := funnel.Steps
// Build step data for the dynamic query builder
steps := make([]struct {
ServiceName string
SpanName string
ContainsError int
LatencyPointer string
Clause string
}, len(funnelSteps))
for i, step := range funnelSteps {
// Build filter clause
clause, err := tracev4.BuildTracesFilterQuery(step.Filters, false)
if err != nil {
return nil, err
}
latencyPointer := step.LatencyPointer
if latencyPointer == "" {
latencyPointer = "start"
}
steps[i] = struct {
ServiceName string
SpanName string
ContainsError int
LatencyPointer string
Clause string
}{
ServiceName: step.ServiceName,
SpanName: step.SpanName,
ContainsError: 0,
LatencyPointer: latencyPointer,
Clause: sanitizeClause(clause),
}
if step.HasErrors {
steps[i].ContainsError = 1
}
}
query := BuildFunnelOverviewQuery(steps, timeRange.StartTime, timeRange.EndTime)
return &v3.ClickHouseQuery{Query: query}, nil
}
func GetFunnelStepAnalytics(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange, stepStart, stepEnd int64) (*v3.ClickHouseQuery, error) {
if stepStart == stepEnd {
return nil, fmt.Errorf("step start and end cannot be the same for /step/overview")
}
funnelSteps := funnel.Steps
// Build step data for the dynamic query builder
steps := make([]struct {
ServiceName string
SpanName string
ContainsError int
LatencyPointer string
LatencyType string
Clause string
}, len(funnelSteps))
for i, step := range funnelSteps {
// Build filter clause
clause, err := tracev4.BuildTracesFilterQuery(step.Filters, false)
if err != nil {
return nil, err
}
latencyPointer := step.LatencyPointer
if latencyPointer == "" {
latencyPointer = "start"
}
steps[i] = struct {
ServiceName string
SpanName string
ContainsError int
LatencyPointer string
LatencyType string
Clause string
}{
ServiceName: step.ServiceName,
SpanName: step.SpanName,
ContainsError: 0,
LatencyPointer: latencyPointer,
LatencyType: step.LatencyType,
Clause: sanitizeClause(clause),
}
if step.HasErrors {
steps[i].ContainsError = 1
}
}
query := BuildFunnelStepOverviewQuery(steps, timeRange.StartTime, timeRange.EndTime, stepStart, stepEnd)
return &v3.ClickHouseQuery{Query: query}, nil
}
func GetStepAnalytics(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange) (*v3.ClickHouseQuery, error) {
funnelSteps := funnel.Steps
// Build step data for the dynamic query builder
steps := make([]struct {
ServiceName string
SpanName string
ContainsError int
Clause string
}, len(funnelSteps))
for i, step := range funnelSteps {
// Build filter clause
clause, err := tracev4.BuildTracesFilterQuery(step.Filters, false)
if err != nil {
return nil, err
}
steps[i] = struct {
ServiceName string
SpanName string
ContainsError int
Clause string
}{
ServiceName: step.ServiceName,
SpanName: step.SpanName,
ContainsError: 0,
Clause: sanitizeClause(clause),
}
if step.HasErrors {
steps[i].ContainsError = 1
}
}
query := BuildFunnelCountQuery(steps, timeRange.StartTime, timeRange.EndTime)
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.BuildTracesFilterQuery(funnelSteps[stepStartOrder].Filters, false)
if err != nil {
return nil, err
}
clauseStep2, err := tracev4.BuildTracesFilterQuery(funnelSteps[stepEndOrder].Filters, false)
if err != nil {
return nil, err
}
// Sanitize clauses
clauseStep1 = sanitizeClause(clauseStep1)
clauseStep2 = sanitizeClause(clauseStep2)
// Get latency pointers with defaults
latencyPointerT1 := funnelSteps[stepStartOrder].LatencyPointer
if latencyPointerT1 == "" {
latencyPointerT1 = "start"
}
latencyPointerT2 := funnelSteps[stepEndOrder].LatencyPointer
if latencyPointerT2 == "" {
latencyPointerT2 = "start"
}
query := BuildFunnelTopSlowTracesQuery(
containsErrorT1,
containsErrorT2,
timeRange.StartTime,
timeRange.EndTime,
funnelSteps[stepStartOrder].ServiceName,
funnelSteps[stepStartOrder].SpanName,
funnelSteps[stepEndOrder].ServiceName,
funnelSteps[stepEndOrder].SpanName,
clauseStep1,
clauseStep2,
latencyPointerT1,
latencyPointerT2,
)
return &v3.ClickHouseQuery{Query: query}, nil
}
// TODO: Showing traces with error which are slow makes little sense as a product. We should show the error spans directly in the funnel chart. Rather showing traces which has drop between steps will be more relevant
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.BuildTracesFilterQuery(funnelSteps[stepStartOrder].Filters, false)
if err != nil {
return nil, err
}
clauseStep2, err := tracev4.BuildTracesFilterQuery(funnelSteps[stepEndOrder].Filters, false)
if err != nil {
return nil, err
}
// Sanitize clauses
clauseStep1 = sanitizeClause(clauseStep1)
clauseStep2 = sanitizeClause(clauseStep2)
// Get latency pointers with defaults
latencyPointerT1 := funnelSteps[stepStartOrder].LatencyPointer
if latencyPointerT1 == "" {
latencyPointerT1 = "start"
}
latencyPointerT2 := funnelSteps[stepEndOrder].LatencyPointer
if latencyPointerT2 == "" {
latencyPointerT2 = "start"
}
query := BuildFunnelTopSlowErrorTracesQuery(
containsErrorT1,
containsErrorT2,
timeRange.StartTime,
timeRange.EndTime,
funnelSteps[stepStartOrder].ServiceName,
funnelSteps[stepStartOrder].SpanName,
funnelSteps[stepEndOrder].ServiceName,
funnelSteps[stepEndOrder].SpanName,
clauseStep1,
clauseStep2,
latencyPointerT1,
latencyPointerT2,
)
return &v3.ClickHouseQuery{Query: query}, nil
}