signoz/pkg/modules/tracefunnel/clickhouse_queries_latency_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

258 lines
8.9 KiB
Go

package tracefunnel
import (
"strings"
"testing"
)
func TestBuildFunnelOverviewQuery_WithLatencyPointer(t *testing.T) {
tests := []struct {
name string
steps []struct {
ServiceName string
SpanName string
ContainsError int
LatencyPointer string
Clause string
}
startTs int64
endTs int64
wantContains []string
wantNotContains []string
}{
{
name: "latency pointer end for first step only",
steps: []struct {
ServiceName string
SpanName string
ContainsError int
LatencyPointer string
Clause string
}{
{ServiceName: "service1", SpanName: "span1", ContainsError: 0, LatencyPointer: "end", Clause: ""},
{ServiceName: "service2", SpanName: "span2", ContainsError: 0, LatencyPointer: "start", Clause: ""},
},
startTs: 1000000000,
endTs: 2000000000,
wantContains: []string{
"minIf(timestamp, resource_string_service$$name = step1.1 AND name = step1.2) + toIntervalNanosecond(minIf(duration_nano, resource_string_service$$name = step1.1 AND name = step1.2)) AS t1_time",
"minIf(timestamp, resource_string_service$$name = step2.1 AND name = step2.2) AS t2_time",
},
},
{
name: "latency pointer end for all steps",
steps: []struct {
ServiceName string
SpanName string
ContainsError int
LatencyPointer string
Clause string
}{
{ServiceName: "service1", SpanName: "span1", ContainsError: 0, LatencyPointer: "end", Clause: ""},
{ServiceName: "service2", SpanName: "span2", ContainsError: 0, LatencyPointer: "end", Clause: ""},
{ServiceName: "service3", SpanName: "span3", ContainsError: 0, LatencyPointer: "end", Clause: ""},
},
startTs: 1000000000,
endTs: 2000000000,
wantContains: []string{
"minIf(timestamp, resource_string_service$$name = step1.1 AND name = step1.2) + toIntervalNanosecond(minIf(duration_nano, resource_string_service$$name = step1.1 AND name = step1.2)) AS t1_time",
"minIf(timestamp, resource_string_service$$name = step2.1 AND name = step2.2) + toIntervalNanosecond(minIf(duration_nano, resource_string_service$$name = step2.1 AND name = step2.2)) AS t2_time",
"minIf(timestamp, resource_string_service$$name = step3.1 AND name = step3.2) + toIntervalNanosecond(minIf(duration_nano, resource_string_service$$name = step3.1 AND name = step3.2)) AS t3_time",
},
},
{
name: "mixed latency pointers",
steps: []struct {
ServiceName string
SpanName string
ContainsError int
LatencyPointer string
Clause string
}{
{ServiceName: "service1", SpanName: "span1", ContainsError: 0, LatencyPointer: "start", Clause: ""},
{ServiceName: "service2", SpanName: "span2", ContainsError: 0, LatencyPointer: "end", Clause: ""},
{ServiceName: "service3", SpanName: "span3", ContainsError: 0, LatencyPointer: "start", Clause: ""},
},
startTs: 1000000000,
endTs: 2000000000,
wantContains: []string{
"minIf(timestamp, resource_string_service$$name = step1.1 AND name = step1.2) AS t1_time",
"minIf(timestamp, resource_string_service$$name = step2.1 AND name = step2.2) + toIntervalNanosecond(minIf(duration_nano, resource_string_service$$name = step2.1 AND name = step2.2)) AS t2_time",
"minIf(timestamp, resource_string_service$$name = step3.1 AND name = step3.2) AS t3_time",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
query := BuildFunnelOverviewQuery(tt.steps, tt.startTs, tt.endTs)
for _, want := range tt.wantContains {
if !strings.Contains(query, want) {
t.Errorf("Query missing expected content: %s", want)
}
}
for _, notWant := range tt.wantNotContains {
if strings.Contains(query, notWant) {
t.Errorf("Query contains unexpected content: %s", notWant)
}
}
})
}
}
func TestBuildFunnelStepOverviewQuery_WithLatencyPointer(t *testing.T) {
tests := []struct {
name string
steps []struct {
ServiceName string
SpanName string
ContainsError int
LatencyPointer string
LatencyType string
Clause string
}
stepStart int64
stepEnd int64
wantContains []string
}{
{
name: "step 1 to 2 with end latency pointers",
steps: []struct {
ServiceName string
SpanName string
ContainsError int
LatencyPointer string
LatencyType string
Clause string
}{
{ServiceName: "service1", SpanName: "span1", ContainsError: 0, LatencyPointer: "end", LatencyType: "p99", Clause: ""},
{ServiceName: "service2", SpanName: "span2", ContainsError: 0, LatencyPointer: "end", LatencyType: "p99", Clause: ""},
},
stepStart: 1,
stepEnd: 2,
wantContains: []string{
"minIf(timestamp, resource_string_service$$name = step1.1 AND name = step1.2) + toIntervalNanosecond(minIf(duration_nano, resource_string_service$$name = step1.1 AND name = step1.2)) AS t1_time",
"minIf(timestamp, resource_string_service$$name = step2.1 AND name = step2.2) + toIntervalNanosecond(minIf(duration_nano, resource_string_service$$name = step2.1 AND name = step2.2)) AS t2_time",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
query := BuildFunnelStepOverviewQuery(tt.steps, 1000000000, 2000000000, tt.stepStart, tt.stepEnd)
for _, want := range tt.wantContains {
if !strings.Contains(query, want) {
t.Errorf("Query missing expected content: %s", want)
}
}
})
}
}
func TestBuildFunnelTopSlowTracesQuery_WithLatencyPointer(t *testing.T) {
tests := []struct {
name string
latencyPointerT1 string
latencyPointerT2 string
wantContains []string
}{
{
name: "both steps with end latency",
latencyPointerT1: "end",
latencyPointerT2: "end",
wantContains: []string{
"minIf(timestamp, resource_string_service$$name = step1.1 AND name = step1.2) + toIntervalNanosecond(minIf(duration_nano, resource_string_service$$name = step1.1 AND name = step1.2)) AS t1_time",
"minIf(timestamp, resource_string_service$$name = step2.1 AND name = step2.2) + toIntervalNanosecond(minIf(duration_nano, resource_string_service$$name = step2.1 AND name = step2.2)) AS t2_time",
},
},
{
name: "first step end, second step start",
latencyPointerT1: "end",
latencyPointerT2: "start",
wantContains: []string{
"minIf(timestamp, resource_string_service$$name = step1.1 AND name = step1.2) + toIntervalNanosecond(minIf(duration_nano, resource_string_service$$name = step1.1 AND name = step1.2)) AS t1_time",
"minIf(timestamp, resource_string_service$$name = step2.1 AND name = step2.2) AS t2_time",
},
},
{
name: "both steps with start latency",
latencyPointerT1: "start",
latencyPointerT2: "start",
wantContains: []string{
"minIf(timestamp, resource_string_service$$name = step1.1 AND name = step1.2) AS t1_time",
"minIf(timestamp, resource_string_service$$name = step2.1 AND name = step2.2) AS t2_time",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
query := BuildFunnelTopSlowTracesQuery(
0, 0,
1000000000, 2000000000,
"service1", "span1",
"service2", "span2",
"", "",
tt.latencyPointerT1,
tt.latencyPointerT2,
)
for _, want := range tt.wantContains {
if !strings.Contains(query, want) {
t.Errorf("Query missing expected content: %s", want)
}
}
})
}
}
func TestBuildFunnelTopSlowErrorTracesQuery_WithLatencyPointer(t *testing.T) {
tests := []struct {
name string
latencyPointerT1 string
latencyPointerT2 string
wantContains []string
}{
{
name: "both steps with end latency",
latencyPointerT1: "end",
latencyPointerT2: "end",
wantContains: []string{
"minIf(timestamp, resource_string_service$$name = step1.1 AND name = step1.2) + toIntervalNanosecond(minIf(duration_nano, resource_string_service$$name = step1.1 AND name = step1.2)) AS t1_time",
"minIf(timestamp, resource_string_service$$name = step2.1 AND name = step2.2) + toIntervalNanosecond(minIf(duration_nano, resource_string_service$$name = step2.1 AND name = step2.2)) AS t2_time",
},
},
{
name: "mixed latency pointers",
latencyPointerT1: "start",
latencyPointerT2: "end",
wantContains: []string{
"minIf(timestamp, resource_string_service$$name = step1.1 AND name = step1.2) AS t1_time",
"minIf(timestamp, resource_string_service$$name = step2.1 AND name = step2.2) + toIntervalNanosecond(minIf(duration_nano, resource_string_service$$name = step2.1 AND name = step2.2)) AS t2_time",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
query := BuildFunnelTopSlowErrorTracesQuery(
0, 0,
1000000000, 2000000000,
"service1", "span1",
"service2", "span2",
"", "",
tt.latencyPointerT1,
tt.latencyPointerT2,
)
for _, want := range tt.wantContains {
if !strings.Contains(query, want) {
t.Errorf("Query missing expected content: %s", want)
}
}
})
}
}