signoz/pkg/modules/tracefunnel/query_test.go
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

271 lines
7.8 KiB
Go

package tracefunnel
import (
"testing"
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"
)
func TestValidateTracesMultipleSteps(t *testing.T) {
tests := []struct {
name string
funnel *tracefunneltypes.StorableFunnel
timeRange tracefunneltypes.TimeRange
expectError bool
}{
{
name: "multi step funnel validation (4 steps)",
funnel: &tracefunneltypes.StorableFunnel{
Steps: []*tracefunneltypes.FunnelStep{
{
ServiceName: "service1",
SpanName: "span1",
HasErrors: false,
Filters: &v3.FilterSet{},
},
{
ServiceName: "service2",
SpanName: "span2",
HasErrors: true,
Filters: &v3.FilterSet{},
},
{
ServiceName: "service3",
SpanName: "span3",
HasErrors: false,
Filters: &v3.FilterSet{},
},
{
ServiceName: "service4",
SpanName: "span4",
HasErrors: true,
Filters: &v3.FilterSet{},
},
},
},
timeRange: tracefunneltypes.TimeRange{
StartTime: 1000000000,
EndTime: 2000000000,
},
expectError: false,
},
{
name: "single step funnel validation (1 step)",
funnel: &tracefunneltypes.StorableFunnel{
Steps: []*tracefunneltypes.FunnelStep{
{
ServiceName: "service1",
SpanName: "span1",
HasErrors: false,
Filters: &v3.FilterSet{},
},
},
},
timeRange: tracefunneltypes.TimeRange{
StartTime: 1000000000,
EndTime: 2000000000,
},
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ValidateTraces(tt.funnel, tt.timeRange)
if tt.expectError && err == nil {
t.Errorf("ValidateTraces() expected error but got none")
}
if !tt.expectError && err != nil {
t.Errorf("ValidateTraces() unexpected error: %v", err)
}
if !tt.expectError && result == nil {
t.Errorf("ValidateTraces() expected result but got nil")
}
if !tt.expectError && result != nil && result.Query == "" {
t.Errorf("ValidateTraces() expected non-empty query")
}
})
}
}
func TestGetFunnelAnalyticsMultipleSteps(t *testing.T) {
tests := []struct {
name string
funnel *tracefunneltypes.StorableFunnel
timeRange tracefunneltypes.TimeRange
expectError bool
}{
{
name: "multi step funnel analytics (5 steps)",
funnel: &tracefunneltypes.StorableFunnel{
Steps: []*tracefunneltypes.FunnelStep{
{
ServiceName: "service1",
SpanName: "span1",
HasErrors: false,
LatencyPointer: "start",
Filters: &v3.FilterSet{},
},
{
ServiceName: "service2",
SpanName: "span2",
HasErrors: true,
LatencyPointer: "end",
Filters: &v3.FilterSet{},
},
{
ServiceName: "service3",
SpanName: "span3",
HasErrors: false,
LatencyPointer: "start",
Filters: &v3.FilterSet{},
},
{
ServiceName: "service4",
SpanName: "span4",
HasErrors: false,
LatencyPointer: "end",
Filters: &v3.FilterSet{},
},
{
ServiceName: "service5",
SpanName: "span5",
HasErrors: true,
LatencyPointer: "start",
Filters: &v3.FilterSet{},
},
},
},
timeRange: tracefunneltypes.TimeRange{
StartTime: 1000000000,
EndTime: 2000000000,
},
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := GetFunnelAnalytics(tt.funnel, tt.timeRange)
if tt.expectError && err == nil {
t.Errorf("GetFunnelAnalytics() expected error but got none")
}
if !tt.expectError && err != nil {
t.Errorf("GetFunnelAnalytics() unexpected error: %v", err)
}
if !tt.expectError && result == nil {
t.Errorf("GetFunnelAnalytics() expected result but got nil")
}
if !tt.expectError && result != nil && result.Query == "" {
t.Errorf("GetFunnelAnalytics() expected non-empty query")
}
})
}
}
func TestGetStepAnalyticsMultipleSteps(t *testing.T) {
funnel := &tracefunneltypes.StorableFunnel{
Steps: []*tracefunneltypes.FunnelStep{
{ServiceName: "s1", SpanName: "sp1", HasErrors: false, Filters: &v3.FilterSet{}},
{ServiceName: "s2", SpanName: "sp2", HasErrors: true, Filters: &v3.FilterSet{}},
{ServiceName: "s3", SpanName: "sp3", HasErrors: false, Filters: &v3.FilterSet{}},
{ServiceName: "s4", SpanName: "sp4", HasErrors: false, Filters: &v3.FilterSet{}},
{ServiceName: "s5", SpanName: "sp5", HasErrors: true, Filters: &v3.FilterSet{}},
},
}
timeRange := tracefunneltypes.TimeRange{
StartTime: 1000000000,
EndTime: 2000000000,
}
result, err := GetStepAnalytics(funnel, timeRange)
if err != nil {
t.Errorf("GetStepAnalytics() unexpected error: %v", err)
}
if result == nil {
t.Errorf("GetStepAnalytics() expected result but got nil")
}
if result != nil && result.Query == "" {
t.Errorf("GetStepAnalytics() expected non-empty query")
}
}
func TestGetFunnelStepAnalyticsMultipleSteps(t *testing.T) {
tests := []struct {
name string
funnel *tracefunneltypes.StorableFunnel
timeRange tracefunneltypes.TimeRange
stepStart int64
stepEnd int64
expectError bool
}{
{
name: "step 2 to 4 in 6-step funnel",
funnel: &tracefunneltypes.StorableFunnel{
Steps: []*tracefunneltypes.FunnelStep{
{ServiceName: "s1", SpanName: "sp1", HasErrors: false, LatencyPointer: "start", LatencyType: "", Filters: &v3.FilterSet{}},
{ServiceName: "s2", SpanName: "sp2", HasErrors: false, LatencyPointer: "start", LatencyType: "p90", Filters: &v3.FilterSet{}},
{ServiceName: "s3", SpanName: "sp3", HasErrors: true, LatencyPointer: "end", LatencyType: "", Filters: &v3.FilterSet{}},
{ServiceName: "s4", SpanName: "sp4", HasErrors: false, LatencyPointer: "start", LatencyType: "p95", Filters: &v3.FilterSet{}},
{ServiceName: "s5", SpanName: "sp5", HasErrors: false, LatencyPointer: "end", LatencyType: "", Filters: &v3.FilterSet{}},
{ServiceName: "s6", SpanName: "sp6", HasErrors: true, LatencyPointer: "start", LatencyType: "", Filters: &v3.FilterSet{}},
},
},
timeRange: tracefunneltypes.TimeRange{
StartTime: 1000000000,
EndTime: 2000000000,
},
stepStart: 2,
stepEnd: 4,
expectError: false,
},
{
name: "invalid same step range",
funnel: &tracefunneltypes.StorableFunnel{
Steps: []*tracefunneltypes.FunnelStep{
{ServiceName: "s1", SpanName: "sp1", HasErrors: false, LatencyPointer: "start", Filters: &v3.FilterSet{}},
{ServiceName: "s2", SpanName: "sp2", HasErrors: false, LatencyPointer: "start", Filters: &v3.FilterSet{}},
},
},
timeRange: tracefunneltypes.TimeRange{
StartTime: 1000000000,
EndTime: 2000000000,
},
stepStart: 1,
stepEnd: 1,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := GetFunnelStepAnalytics(tt.funnel, tt.timeRange, tt.stepStart, tt.stepEnd)
if tt.expectError && err == nil {
t.Errorf("GetFunnelStepAnalytics() expected error but got none")
}
if !tt.expectError && err != nil {
t.Errorf("GetFunnelStepAnalytics() unexpected error: %v", err)
}
if !tt.expectError && result == nil {
t.Errorf("GetFunnelStepAnalytics() expected result but got nil")
}
if !tt.expectError && result != nil && result.Query == "" {
t.Errorf("GetFunnelStepAnalytics() expected non-empty query")
}
})
}
}
// Mock the tracev4.BuildTracesFilterQuery function since it's external
func init() {
// This would normally be handled by the actual implementation
// For testing purposes, we'll assume it returns an empty string
_ = tracev4.BuildTracesFilterQuery
}