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 }