mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 23:47:12 +00:00
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>
This commit is contained in:
parent
f53a13e7fa
commit
f2abddd2ed
File diff suppressed because it is too large
Load Diff
258
pkg/modules/tracefunnel/clickhouse_queries_latency_test.go
Normal file
258
pkg/modules/tracefunnel/clickhouse_queries_latency_test.go
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
392
pkg/modules/tracefunnel/clickhouse_queries_test.go
Normal file
392
pkg/modules/tracefunnel/clickhouse_queries_test.go
Normal file
@ -0,0 +1,392 @@
|
|||||||
|
package tracefunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildFunnelValidationQuery(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
steps []struct {
|
||||||
|
ServiceName string
|
||||||
|
SpanName string
|
||||||
|
ContainsError int
|
||||||
|
Clause string
|
||||||
|
}
|
||||||
|
startTs int64
|
||||||
|
endTs int64
|
||||||
|
wantContains []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "multi step funnel (2 steps)",
|
||||||
|
steps: []struct {
|
||||||
|
ServiceName string
|
||||||
|
SpanName string
|
||||||
|
ContainsError int
|
||||||
|
Clause string
|
||||||
|
}{
|
||||||
|
{ServiceName: "service1", SpanName: "span1", ContainsError: 0, Clause: "AND attr1 = 'value1'"},
|
||||||
|
{ServiceName: "service2", SpanName: "span2", ContainsError: 1, Clause: "AND attr2 = 'value2'"},
|
||||||
|
},
|
||||||
|
startTs: 1000000000,
|
||||||
|
endTs: 2000000000,
|
||||||
|
wantContains: []string{
|
||||||
|
"('service1','span1') AS step1",
|
||||||
|
"('service2','span2') AS step2",
|
||||||
|
"0 AS contains_error_t1",
|
||||||
|
"1 AS contains_error_t2",
|
||||||
|
"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",
|
||||||
|
"AND attr1 = 'value1'",
|
||||||
|
"AND attr2 = 'value2'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multi step funnel (3 steps)",
|
||||||
|
steps: []struct {
|
||||||
|
ServiceName string
|
||||||
|
SpanName string
|
||||||
|
ContainsError int
|
||||||
|
Clause string
|
||||||
|
}{
|
||||||
|
{ServiceName: "service1", SpanName: "span1", ContainsError: 0, Clause: ""},
|
||||||
|
{ServiceName: "service2", SpanName: "span2", ContainsError: 1, Clause: "AND attr2 = 'value2'"},
|
||||||
|
{ServiceName: "service3", SpanName: "span3", ContainsError: 0, Clause: "AND attr3 = 'value3'"},
|
||||||
|
},
|
||||||
|
startTs: 1000000000,
|
||||||
|
endTs: 2000000000,
|
||||||
|
wantContains: []string{
|
||||||
|
"('service1','span1') AS step1",
|
||||||
|
"('service2','span2') AS step2",
|
||||||
|
"('service3','span3') AS step3",
|
||||||
|
"minIf(timestamp, resource_string_service$$name = step3.1 AND name = step3.2) AS t3_time",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "five step funnel",
|
||||||
|
steps: []struct {
|
||||||
|
ServiceName string
|
||||||
|
SpanName string
|
||||||
|
ContainsError int
|
||||||
|
Clause string
|
||||||
|
}{
|
||||||
|
{ServiceName: "service1", SpanName: "span1", ContainsError: 0, Clause: ""},
|
||||||
|
{ServiceName: "service2", SpanName: "span2", ContainsError: 0, Clause: ""},
|
||||||
|
{ServiceName: "service3", SpanName: "span3", ContainsError: 1, Clause: ""},
|
||||||
|
{ServiceName: "service4", SpanName: "span4", ContainsError: 0, Clause: ""},
|
||||||
|
{ServiceName: "service5", SpanName: "span5", ContainsError: 1, Clause: ""},
|
||||||
|
},
|
||||||
|
startTs: 1000000000,
|
||||||
|
endTs: 2000000000,
|
||||||
|
wantContains: []string{
|
||||||
|
"('service5','span5') AS step5",
|
||||||
|
"minIf(timestamp, resource_string_service$$name = step5.1 AND name = step5.2) AS t5_time",
|
||||||
|
"1 AS contains_error_t5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := BuildFunnelValidationQuery(tt.steps, tt.startTs, tt.endTs)
|
||||||
|
|
||||||
|
for _, want := range tt.wantContains {
|
||||||
|
if !strings.Contains(got, want) {
|
||||||
|
t.Errorf("BuildFunnelValidationQuery() missing expected string: %q", want)
|
||||||
|
t.Logf("Got query:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildFunnelOverviewQuery(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
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "multi step funnel with latency (2 steps)",
|
||||||
|
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: 1, LatencyPointer: "end", Clause: ""},
|
||||||
|
},
|
||||||
|
startTs: 1000000000,
|
||||||
|
endTs: 2000000000,
|
||||||
|
wantContains: []string{
|
||||||
|
"'start' AS latency_pointer_t1",
|
||||||
|
"'end' AS latency_pointer_t2",
|
||||||
|
"count(DISTINCT CASE WHEN t2_time > t1_time THEN trace_id END) AS total_s2_spans",
|
||||||
|
"avgIf((toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time))/1e6",
|
||||||
|
"quantileIf(0.99)((toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time))/1e6",
|
||||||
|
"round(if(total_s1_spans > 0, total_s2_spans * 100.0 / total_s1_spans, 0), 2) AS conversion_rate",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "four step funnel",
|
||||||
|
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: "start", Clause: ""},
|
||||||
|
{ServiceName: "service3", SpanName: "span3", ContainsError: 0, LatencyPointer: "start", Clause: ""},
|
||||||
|
{ServiceName: "service4", SpanName: "span4", ContainsError: 1, LatencyPointer: "end", Clause: ""},
|
||||||
|
},
|
||||||
|
startTs: 1000000000,
|
||||||
|
endTs: 2000000000,
|
||||||
|
wantContains: []string{
|
||||||
|
"('service4','span4') AS step4",
|
||||||
|
"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 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 t4_time > t3_time THEN trace_id END) AS total_s4_spans",
|
||||||
|
"round(if(total_s1_spans > 0, total_s4_spans * 100.0 / total_s1_spans, 0), 2) AS conversion_rate",
|
||||||
|
"avgIf((toUnixTimestamp64Nano(t4_time) - toUnixTimestamp64Nano(t1_time))/1e6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := BuildFunnelOverviewQuery(tt.steps, tt.startTs, tt.endTs)
|
||||||
|
|
||||||
|
for _, want := range tt.wantContains {
|
||||||
|
if !strings.Contains(got, want) {
|
||||||
|
t.Errorf("BuildFunnelOverviewQuery() missing expected string: %q", want)
|
||||||
|
t.Logf("Got query:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildFunnelCountQuery(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
steps []struct {
|
||||||
|
ServiceName string
|
||||||
|
SpanName string
|
||||||
|
ContainsError int
|
||||||
|
Clause string
|
||||||
|
}
|
||||||
|
startTs int64
|
||||||
|
endTs int64
|
||||||
|
wantContains []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "multi step funnel count (3 steps)",
|
||||||
|
steps: []struct {
|
||||||
|
ServiceName string
|
||||||
|
SpanName string
|
||||||
|
ContainsError int
|
||||||
|
Clause string
|
||||||
|
}{
|
||||||
|
{ServiceName: "service1", SpanName: "span1", ContainsError: 0, Clause: ""},
|
||||||
|
{ServiceName: "service2", SpanName: "span2", ContainsError: 1, Clause: ""},
|
||||||
|
{ServiceName: "service3", SpanName: "span3", ContainsError: 0, Clause: ""},
|
||||||
|
},
|
||||||
|
startTs: 1000000000,
|
||||||
|
endTs: 2000000000,
|
||||||
|
wantContains: []string{
|
||||||
|
"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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "five step funnel count",
|
||||||
|
steps: []struct {
|
||||||
|
ServiceName string
|
||||||
|
SpanName string
|
||||||
|
ContainsError int
|
||||||
|
Clause string
|
||||||
|
}{
|
||||||
|
{ServiceName: "s1", SpanName: "sp1", ContainsError: 0, Clause: ""},
|
||||||
|
{ServiceName: "s2", SpanName: "sp2", ContainsError: 0, Clause: ""},
|
||||||
|
{ServiceName: "s3", SpanName: "sp3", ContainsError: 0, Clause: ""},
|
||||||
|
{ServiceName: "s4", SpanName: "sp4", ContainsError: 0, Clause: ""},
|
||||||
|
{ServiceName: "s5", SpanName: "sp5", ContainsError: 1, Clause: ""},
|
||||||
|
},
|
||||||
|
startTs: 1000000000,
|
||||||
|
endTs: 2000000000,
|
||||||
|
wantContains: []string{
|
||||||
|
"count(DISTINCT CASE WHEN t2_time > t1_time AND t3_time > t2_time AND t4_time > t3_time AND t5_time > t4_time THEN trace_id END) AS total_s5_spans",
|
||||||
|
"toUInt8(anyIf(has_error, resource_string_service$$name = step5.1 AND name = step5.2)) AS t5_error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := BuildFunnelCountQuery(tt.steps, tt.startTs, tt.endTs)
|
||||||
|
|
||||||
|
for _, want := range tt.wantContains {
|
||||||
|
if !strings.Contains(got, want) {
|
||||||
|
t.Errorf("BuildFunnelCountQuery() missing expected string: %q", want)
|
||||||
|
t.Logf("Got query:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildFunnelStepOverviewQuery(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
steps []struct {
|
||||||
|
ServiceName string
|
||||||
|
SpanName string
|
||||||
|
ContainsError int
|
||||||
|
LatencyPointer string
|
||||||
|
LatencyType string
|
||||||
|
Clause string
|
||||||
|
}
|
||||||
|
startTs int64
|
||||||
|
endTs int64
|
||||||
|
stepStart int64
|
||||||
|
stepEnd int64
|
||||||
|
wantContains []string
|
||||||
|
wantFallback bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "step 1 to 2 transition",
|
||||||
|
steps: []struct {
|
||||||
|
ServiceName string
|
||||||
|
SpanName string
|
||||||
|
ContainsError int
|
||||||
|
LatencyPointer string
|
||||||
|
LatencyType string
|
||||||
|
Clause string
|
||||||
|
}{
|
||||||
|
{ServiceName: "service1", SpanName: "span1", ContainsError: 0, LatencyPointer: "start", LatencyType: "", Clause: ""},
|
||||||
|
{ServiceName: "service2", SpanName: "span2", ContainsError: 1, LatencyPointer: "end", LatencyType: "p95", Clause: ""},
|
||||||
|
{ServiceName: "service3", SpanName: "span3", ContainsError: 0, LatencyPointer: "start", LatencyType: "", Clause: ""},
|
||||||
|
},
|
||||||
|
startTs: 1000000000,
|
||||||
|
endTs: 2000000000,
|
||||||
|
stepStart: 1,
|
||||||
|
stepEnd: 2,
|
||||||
|
wantContains: []string{
|
||||||
|
"round(total_s2_spans * 100.0 / total_s1_spans, 2) AS conversion_rate",
|
||||||
|
"quantileIf(0.95)",
|
||||||
|
"t2_time > t1_time",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "step 2 to 4 transition in 5-step funnel",
|
||||||
|
steps: []struct {
|
||||||
|
ServiceName string
|
||||||
|
SpanName string
|
||||||
|
ContainsError int
|
||||||
|
LatencyPointer string
|
||||||
|
LatencyType string
|
||||||
|
Clause string
|
||||||
|
}{
|
||||||
|
{ServiceName: "s1", SpanName: "sp1", ContainsError: 0, LatencyPointer: "start", LatencyType: "", Clause: ""},
|
||||||
|
{ServiceName: "s2", SpanName: "sp2", ContainsError: 0, LatencyPointer: "start", LatencyType: "", Clause: ""},
|
||||||
|
{ServiceName: "s3", SpanName: "sp3", ContainsError: 0, LatencyPointer: "start", LatencyType: "", Clause: ""},
|
||||||
|
{ServiceName: "s4", SpanName: "sp4", ContainsError: 0, LatencyPointer: "start", LatencyType: "p90", Clause: ""},
|
||||||
|
{ServiceName: "s5", SpanName: "sp5", ContainsError: 0, LatencyPointer: "start", LatencyType: "", Clause: ""},
|
||||||
|
},
|
||||||
|
startTs: 1000000000,
|
||||||
|
endTs: 2000000000,
|
||||||
|
stepStart: 2,
|
||||||
|
stepEnd: 4,
|
||||||
|
wantContains: []string{
|
||||||
|
"round(total_s4_spans * 100.0 / total_s2_spans, 2) AS conversion_rate",
|
||||||
|
"t3_time > t2_time AND t4_time > t3_time",
|
||||||
|
"quantileIf(0.90)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid step range",
|
||||||
|
steps: []struct {
|
||||||
|
ServiceName string
|
||||||
|
SpanName string
|
||||||
|
ContainsError int
|
||||||
|
LatencyPointer string
|
||||||
|
LatencyType string
|
||||||
|
Clause string
|
||||||
|
}{
|
||||||
|
{ServiceName: "s1", SpanName: "sp1", ContainsError: 0, LatencyPointer: "start", LatencyType: "", Clause: ""},
|
||||||
|
{ServiceName: "s2", SpanName: "sp2", ContainsError: 0, LatencyPointer: "start", LatencyType: "", Clause: ""},
|
||||||
|
},
|
||||||
|
startTs: 1000000000,
|
||||||
|
endTs: 2000000000,
|
||||||
|
stepStart: 2,
|
||||||
|
stepEnd: 2, // same step - invalid
|
||||||
|
wantFallback: true,
|
||||||
|
wantContains: []string{
|
||||||
|
"SELECT 0 AS conversion_rate, 0 AS avg_rate, 0 AS errors, 0 AS avg_duration, 0 AS latency;",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := BuildFunnelStepOverviewQuery(tt.steps, tt.startTs, tt.endTs, tt.stepStart, tt.stepEnd)
|
||||||
|
|
||||||
|
for _, want := range tt.wantContains {
|
||||||
|
if !strings.Contains(got, want) {
|
||||||
|
t.Errorf("BuildFunnelStepOverviewQuery() missing expected string: %q", want)
|
||||||
|
t.Logf("Got query:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantFallback && !strings.Contains(got, "SELECT 0 AS conversion_rate") {
|
||||||
|
t.Errorf("BuildFunnelStepOverviewQuery() expected fallback query for invalid step range")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTemporalOrderingLogic(t *testing.T) {
|
||||||
|
// Test that temporal ordering is correctly built for multiple steps
|
||||||
|
query := BuildFunnelOverviewQuery([]struct {
|
||||||
|
ServiceName string
|
||||||
|
SpanName string
|
||||||
|
ContainsError int
|
||||||
|
LatencyPointer string
|
||||||
|
Clause string
|
||||||
|
}{
|
||||||
|
{ServiceName: "s1", SpanName: "sp1", ContainsError: 0, LatencyPointer: "start", Clause: ""},
|
||||||
|
{ServiceName: "s2", SpanName: "sp2", ContainsError: 0, LatencyPointer: "start", Clause: ""},
|
||||||
|
{ServiceName: "s3", SpanName: "sp3", ContainsError: 0, LatencyPointer: "start", Clause: ""},
|
||||||
|
{ServiceName: "s4", SpanName: "sp4", ContainsError: 0, LatencyPointer: "start", Clause: ""},
|
||||||
|
}, 1000000000, 2000000000)
|
||||||
|
|
||||||
|
// Check that each step has proper temporal ordering (cumulative format)
|
||||||
|
temporalChecks := []string{
|
||||||
|
"t2_time > t1_time",
|
||||||
|
"t2_time > t1_time AND t3_time > t2_time",
|
||||||
|
"t2_time > t1_time AND t3_time > t2_time AND t4_time > t3_time",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, check := range temporalChecks {
|
||||||
|
if !strings.Contains(query, check) {
|
||||||
|
t.Errorf("Missing temporal ordering check: %s", check)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,77 +22,41 @@ func sanitizeClause(clause string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ValidateTraces(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange) (*v3.ClickHouseQuery, error) {
|
func ValidateTraces(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange) (*v3.ClickHouseQuery, error) {
|
||||||
var query string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
funnelSteps := funnel.Steps
|
funnelSteps := funnel.Steps
|
||||||
containsErrorT1 := 0
|
|
||||||
containsErrorT2 := 0
|
|
||||||
containsErrorT3 := 0
|
|
||||||
|
|
||||||
if funnelSteps[0].HasErrors {
|
// Build step data for the dynamic query builder
|
||||||
containsErrorT1 = 1
|
steps := make([]struct {
|
||||||
}
|
ServiceName string
|
||||||
if funnelSteps[1].HasErrors {
|
SpanName string
|
||||||
containsErrorT2 = 1
|
ContainsError int
|
||||||
}
|
Clause string
|
||||||
if len(funnel.Steps) > 2 && funnelSteps[2].HasErrors {
|
}, len(funnelSteps))
|
||||||
containsErrorT3 = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build filter clauses for each step
|
for i, step := range funnelSteps {
|
||||||
clauseStep1, err := tracev4.BuildTracesFilterQuery(funnelSteps[0].Filters, false)
|
// Build filter clause
|
||||||
if err != nil {
|
clause, err := tracev4.BuildTracesFilterQuery(step.Filters, false)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
clauseStep2, err := tracev4.BuildTracesFilterQuery(funnelSteps[1].Filters, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
clauseStep3 := ""
|
|
||||||
if len(funnel.Steps) > 2 {
|
|
||||||
clauseStep3, err = tracev4.BuildTracesFilterQuery(funnelSteps[2].Filters, false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize clauses
|
query := BuildFunnelValidationQuery(steps, timeRange.StartTime, timeRange.EndTime)
|
||||||
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{
|
return &v3.ClickHouseQuery{
|
||||||
Query: query,
|
Query: query,
|
||||||
@ -100,280 +64,144 @@ func ValidateTraces(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunn
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetFunnelAnalytics(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange) (*v3.ClickHouseQuery, error) {
|
func GetFunnelAnalytics(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange) (*v3.ClickHouseQuery, error) {
|
||||||
var query string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
funnelSteps := funnel.Steps
|
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 {
|
// Build step data for the dynamic query builder
|
||||||
containsErrorT1 = 1
|
steps := make([]struct {
|
||||||
}
|
ServiceName string
|
||||||
if funnelSteps[1].HasErrors {
|
SpanName string
|
||||||
containsErrorT2 = 1
|
ContainsError int
|
||||||
}
|
LatencyPointer string
|
||||||
if len(funnel.Steps) > 2 && funnelSteps[2].HasErrors {
|
Clause string
|
||||||
containsErrorT3 = 1
|
}, len(funnelSteps))
|
||||||
}
|
|
||||||
|
|
||||||
// Build filter clauses for each step
|
for i, step := range funnelSteps {
|
||||||
clauseStep1, err := tracev4.BuildTracesFilterQuery(funnelSteps[0].Filters, false)
|
// Build filter clause
|
||||||
if err != nil {
|
clause, err := tracev4.BuildTracesFilterQuery(step.Filters, false)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
clauseStep2, err := tracev4.BuildTracesFilterQuery(funnelSteps[1].Filters, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
clauseStep3 := ""
|
|
||||||
if len(funnel.Steps) > 2 {
|
|
||||||
clauseStep3, err = tracev4.BuildTracesFilterQuery(funnelSteps[2].Filters, false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize clauses
|
query := BuildFunnelOverviewQuery(steps, timeRange.StartTime, timeRange.EndTime)
|
||||||
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
|
return &v3.ClickHouseQuery{Query: query}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFunnelStepAnalytics(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange, stepStart, stepEnd int64) (*v3.ClickHouseQuery, error) {
|
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 {
|
if stepStart == stepEnd {
|
||||||
return nil, fmt.Errorf("step start and end cannot be the same for /step/overview")
|
return nil, fmt.Errorf("step start and end cannot be the same for /step/overview")
|
||||||
}
|
}
|
||||||
|
|
||||||
if funnelSteps[0].HasErrors {
|
funnelSteps := funnel.Steps
|
||||||
containsErrorT1 = 1
|
|
||||||
}
|
|
||||||
if funnelSteps[1].HasErrors {
|
|
||||||
containsErrorT2 = 1
|
|
||||||
}
|
|
||||||
if len(funnel.Steps) > 2 && funnelSteps[2].HasErrors {
|
|
||||||
containsErrorT3 = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if funnelSteps[1].LatencyType != "" {
|
// Build step data for the dynamic query builder
|
||||||
latency := strings.ToLower(funnelSteps[1].LatencyType)
|
steps := make([]struct {
|
||||||
if latency == "p90" {
|
ServiceName string
|
||||||
latencyTypeT2 = "0.90"
|
SpanName string
|
||||||
} else if latency == "p95" {
|
ContainsError int
|
||||||
latencyTypeT2 = "0.95"
|
LatencyPointer string
|
||||||
} else {
|
LatencyType string
|
||||||
latencyTypeT2 = "0.99"
|
Clause string
|
||||||
}
|
}, len(funnelSteps))
|
||||||
}
|
|
||||||
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
|
for i, step := range funnelSteps {
|
||||||
clauseStep1, err := tracev4.BuildTracesFilterQuery(funnelSteps[0].Filters, false)
|
// Build filter clause
|
||||||
if err != nil {
|
clause, err := tracev4.BuildTracesFilterQuery(step.Filters, false)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
clauseStep2, err := tracev4.BuildTracesFilterQuery(funnelSteps[1].Filters, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
clauseStep3 := ""
|
|
||||||
if len(funnel.Steps) > 2 {
|
|
||||||
clauseStep3, err = tracev4.BuildTracesFilterQuery(funnelSteps[2].Filters, false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize clauses
|
query := BuildFunnelStepOverviewQuery(steps, timeRange.StartTime, timeRange.EndTime, stepStart, stepEnd)
|
||||||
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
|
return &v3.ClickHouseQuery{Query: query}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetStepAnalytics(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange) (*v3.ClickHouseQuery, error) {
|
func GetStepAnalytics(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange) (*v3.ClickHouseQuery, error) {
|
||||||
var query string
|
|
||||||
|
|
||||||
funnelSteps := funnel.Steps
|
funnelSteps := funnel.Steps
|
||||||
containsErrorT1 := 0
|
|
||||||
containsErrorT2 := 0
|
|
||||||
containsErrorT3 := 0
|
|
||||||
|
|
||||||
if funnelSteps[0].HasErrors {
|
// Build step data for the dynamic query builder
|
||||||
containsErrorT1 = 1
|
steps := make([]struct {
|
||||||
}
|
ServiceName string
|
||||||
if funnelSteps[1].HasErrors {
|
SpanName string
|
||||||
containsErrorT2 = 1
|
ContainsError int
|
||||||
}
|
Clause string
|
||||||
if len(funnel.Steps) > 2 && funnelSteps[2].HasErrors {
|
}, len(funnelSteps))
|
||||||
containsErrorT3 = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build filter clauses for each step
|
for i, step := range funnelSteps {
|
||||||
clauseStep1, err := tracev4.BuildTracesFilterQuery(funnelSteps[0].Filters, false)
|
// Build filter clause
|
||||||
if err != nil {
|
clause, err := tracev4.BuildTracesFilterQuery(step.Filters, false)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
clauseStep2, err := tracev4.BuildTracesFilterQuery(funnelSteps[1].Filters, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
clauseStep3 := ""
|
|
||||||
if len(funnel.Steps) > 2 {
|
|
||||||
clauseStep3, err = tracev4.BuildTracesFilterQuery(funnelSteps[2].Filters, false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize clauses
|
query := BuildFunnelCountQuery(steps, timeRange.StartTime, timeRange.EndTime)
|
||||||
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{
|
return &v3.ClickHouseQuery{
|
||||||
Query: query,
|
Query: query,
|
||||||
@ -412,7 +240,17 @@ func GetSlowestTraces(funnel *tracefunneltypes.StorableFunnel, timeRange tracefu
|
|||||||
clauseStep1 = sanitizeClause(clauseStep1)
|
clauseStep1 = sanitizeClause(clauseStep1)
|
||||||
clauseStep2 = sanitizeClause(clauseStep2)
|
clauseStep2 = sanitizeClause(clauseStep2)
|
||||||
|
|
||||||
query := BuildTwoStepFunnelTopSlowTracesQuery(
|
// Get latency pointers with defaults
|
||||||
|
latencyPointerT1 := funnelSteps[stepStartOrder].LatencyPointer
|
||||||
|
if latencyPointerT1 == "" {
|
||||||
|
latencyPointerT1 = "start"
|
||||||
|
}
|
||||||
|
latencyPointerT2 := funnelSteps[stepEndOrder].LatencyPointer
|
||||||
|
if latencyPointerT2 == "" {
|
||||||
|
latencyPointerT2 = "start"
|
||||||
|
}
|
||||||
|
|
||||||
|
query := BuildFunnelTopSlowTracesQuery(
|
||||||
containsErrorT1,
|
containsErrorT1,
|
||||||
containsErrorT2,
|
containsErrorT2,
|
||||||
timeRange.StartTime,
|
timeRange.StartTime,
|
||||||
@ -423,10 +261,13 @@ func GetSlowestTraces(funnel *tracefunneltypes.StorableFunnel, timeRange tracefu
|
|||||||
funnelSteps[stepEndOrder].SpanName,
|
funnelSteps[stepEndOrder].SpanName,
|
||||||
clauseStep1,
|
clauseStep1,
|
||||||
clauseStep2,
|
clauseStep2,
|
||||||
|
latencyPointerT1,
|
||||||
|
latencyPointerT2,
|
||||||
)
|
)
|
||||||
return &v3.ClickHouseQuery{Query: query}, nil
|
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) {
|
func GetErroredTraces(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange, stepStart, stepEnd int64) (*v3.ClickHouseQuery, error) {
|
||||||
funnelSteps := funnel.Steps
|
funnelSteps := funnel.Steps
|
||||||
containsErrorT1 := 0
|
containsErrorT1 := 0
|
||||||
@ -459,7 +300,17 @@ func GetErroredTraces(funnel *tracefunneltypes.StorableFunnel, timeRange tracefu
|
|||||||
clauseStep1 = sanitizeClause(clauseStep1)
|
clauseStep1 = sanitizeClause(clauseStep1)
|
||||||
clauseStep2 = sanitizeClause(clauseStep2)
|
clauseStep2 = sanitizeClause(clauseStep2)
|
||||||
|
|
||||||
query := BuildTwoStepFunnelTopSlowErrorTracesQuery(
|
// Get latency pointers with defaults
|
||||||
|
latencyPointerT1 := funnelSteps[stepStartOrder].LatencyPointer
|
||||||
|
if latencyPointerT1 == "" {
|
||||||
|
latencyPointerT1 = "start"
|
||||||
|
}
|
||||||
|
latencyPointerT2 := funnelSteps[stepEndOrder].LatencyPointer
|
||||||
|
if latencyPointerT2 == "" {
|
||||||
|
latencyPointerT2 = "start"
|
||||||
|
}
|
||||||
|
|
||||||
|
query := BuildFunnelTopSlowErrorTracesQuery(
|
||||||
containsErrorT1,
|
containsErrorT1,
|
||||||
containsErrorT2,
|
containsErrorT2,
|
||||||
timeRange.StartTime,
|
timeRange.StartTime,
|
||||||
@ -470,6 +321,8 @@ func GetErroredTraces(funnel *tracefunneltypes.StorableFunnel, timeRange tracefu
|
|||||||
funnelSteps[stepEndOrder].SpanName,
|
funnelSteps[stepEndOrder].SpanName,
|
||||||
clauseStep1,
|
clauseStep1,
|
||||||
clauseStep2,
|
clauseStep2,
|
||||||
|
latencyPointerT1,
|
||||||
|
latencyPointerT2,
|
||||||
)
|
)
|
||||||
return &v3.ClickHouseQuery{Query: query}, nil
|
return &v3.ClickHouseQuery{Query: query}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
271
pkg/modules/tracefunnel/query_test.go
Normal file
271
pkg/modules/tracefunnel/query_test.go
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user