Skip to content

Trace Funnels: analytics endpoints return HTTP 500 (unsupported value: NaN) when zero traces complete a step transition — expected 0% conversion #12143

Description

@wiz-abhi

Bug description

When a Trace Funnel has a step transition that zero traces complete (e.g. a step's (service_name, span_name) matches no spans in the window, or no trace satisfies the temporal ordering t2 > t1), the funnel analytics endpoints return HTTP 500 instead of reporting 0% conversion:

  • POST /api/v1/trace-funnels/{funnel_id}/analytics/overview500 with body app.ApiResponse.Data: []*v3.Row: v3.Row.Data: unsupported value: NaN
  • POST /api/v1/trace-funnels/{funnel_id}/analytics/steps/overview500 with the same error

"Nothing reached this step" is arguably the most important answer a funnel can give, and it's exactly the case that errors out.

Root cause

The generated ClickHouse aggregates produce NaN on empty sets, and Go's encoding/json cannot marshal NaN, so the handler 500s. In pkg/modules/tracefunnel/clickhouse_queries.go:

  1. BuildFunnelOverviewQuery — the conversion division is already guarded:

    round(if(total_s1_spans > 0, total_sN_spans * 100.0 / total_s1_spans, 0), 2) AS conversion_rate

    …but avg_duration and latency are not:

    avgIf((toUnixTimestamp64Nano(tN_time) - toUnixTimestamp64Nano(t1_time))/1e6, <full-funnel condition>) AS avg_duration,
    quantileIf(0.99)(...) AS latency

    When zero traces satisfy the full-funnel condition, ClickHouse returns NaN for both → JSON marshal fails → 500. (So a funnel whose conversion correctly computes as 0 still 500s because of the latency fields.)

  2. BuildFunnelStepOverviewQuery — has the same unguarded avgIf/quantileIf, plus its conversion division lacks the guard that BuildFunnelOverviewQuery already has:

    round(total_sEnd_spans * 100.0 / total_sStart_spans, 2) AS conversion_rate

    If the start step matches zero traces this is 0 * 100.0 / 0NaN as well.

How to reproduce

On a self-hosted v0.132.2 with any traces present (times: timestamp in ms, analytics windows in ns):

# 1. Create a funnel
curl -X POST http://localhost:8080/api/v1/trace-funnels/new \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"funnel_name":"nan-repro","timestamp":1784219000000}'

# 2. Configure steps where step 2 matches an existing span but step 3 matches nothing
#    (or nothing that occurs after step 2 within the same trace)
curl -X PUT http://localhost:8080/api/v1/trace-funnels/steps/update \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"funnel_id":"<id>","timestamp":1784219000000,"steps":[
    {"step_order":1,"service_name":"my-svc","span_name":"existing-span-a","filters":{"items":[],"op":"AND"},"latency_pointer":"start","has_errors":false},
    {"step_order":2,"service_name":"my-svc","span_name":"existing-span-b","filters":{"items":[],"op":"AND"},"latency_pointer":"start","has_errors":false},
    {"step_order":3,"service_name":"my-svc","span_name":"does-not-exist","filters":{"items":[],"op":"AND"},"latency_pointer":"start","has_errors":false}]}'

# 3. Overview → HTTP 500
curl -X POST http://localhost:8080/api/v1/trace-funnels/<id>/analytics/overview \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"start_time":1781627000000000000,"end_time":1784219000000000000}'
# → app.ApiResponse.Data: []*v3.Row: v3.Row.Data: unsupported value: NaN

# 4. Step transition into the empty step → HTTP 500
curl -X POST http://localhost:8080/api/v1/trace-funnels/<id>/analytics/steps/overview \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"start_time":1781627000000000000,"end_time":1784219000000000000,"step_start":2,"step_end":3}'

Control check confirming the trigger: with the same funnel re-pointed so step 3 matches spans (/analytics/steps reporting total_s3_spans: 5 instead of 0), both endpoints return 200 with sane numbers (e.g. conversion_rate: 22.73). The 500 appears exactly when a step count drops to zero.

Expected behavior

200 with conversion_rate: 0 (and avg_duration/latency as 0 or null) — a funnel step that nothing reaches is a valid, meaningful result, not an internal error.

Suggested fix

Wrap the empty-set aggregates in both builders, e.g. ifNaN(avgIf(...), 0) / ifNaN(quantileIf(...)(...), 0), and add the missing zero-guard to BuildFunnelStepOverviewQuery's conversion division, mirroring the guard that already exists in BuildFunnelOverviewQuery. Happy to send a PR for this.

Version information

  • SigNoz version: v0.132.2 (/api/v1/version{"version":"v0.132.2","ee":"Y","setupCompleted":true}), self-hosted via Foundry (docker compose flavor)
  • OS: Windows 11, Docker Desktop (WSL2 backend)

Thank you for your bug report – we love squashing them!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions