Split out of #313 so that issue can close with #319. This is the half of #313 that is not a defect in the code as written — it is an open product decision.
Why it is not a regression
#313 framed the method filter as having "silently become case-sensitive" in c7a5dcae. That is not what happened:
|
embedded (SQLite/DuckDB) |
ClickHouse |
| before #290 |
no method filter |
no method filter |
#290 (89a3e4dc) |
LIKE 'GET %' — case-insensitive |
startsWith(endpoint, 'GET ') — case-sensitive |
c7a5dcae |
SUBSTR(...) = 'GET ' — case-sensitive |
unchanged |
The ~24 hours between #290 and c7a5dcae were the parity break; c7a5dcae closed it by aligning the embedded backends onto ClickHouse. It also shipped TestEndpointRepository_FindGroupedByEndpoint_MethodFilter, which inserts get /api/lowercase and asserts it is excluded — the exclusion is deliberate and tested. SUBSTR over LIKE is independently worth keeping: methodFilter is user input and LIKE would let % and _ through as wildcards.
The actual problem
Lowercase-stored methods cannot be reached by any filter option.
getHTTPEndpoint (backend/app/controllers/otelcontrollers/trace_converter.go:462) concatenates http.request.method verbatim with no ToUpper:
method := getStringAttribute(attrs, "http.request.method")
if method == "" {
method = getStringAttribute(attrs, "http.method")
}
...
return method + " " + route
So get /api/orders rows really can exist. Meanwhile METHOD_OPTIONS (frontend/src/lib/components/ui/endpoint-filter/methods.ts) offers only the 7 canonical values, which normalizeMethodFilter uppercases. No dropdown selection can ever match such a row — it is visible in the unfiltered list and vanishes under every filter.
Options
- Leave it. RFC 9110 says method tokens are case-sensitive, and the list already groups
get /x and GET /x as two separate endpoints, so the filter treating them as distinct is internally consistent.
- Case-insensitive filter —
UPPER(SUBSTR(...)) = :method on the embedded backends, startsWith(upper(...), ?) on ClickHouse. Keeps the no-wildcard property, matches how search already behaves in the same clause builder (INSTR(LOWER(...), LOWER(:search))), and reaches rows already stored. Costs the deliberate test above.
- Normalize at ingest —
ToUpper the method in getHTTPEndpoint. Fixes the cause rather than compensating at query time in three SQL dialects, but does not reach rows already stored, and merges two currently-distinct endpoint groups.
Option 3 is the one I'd pick — it addresses the source — with the caveat that existing rows stay unreachable unless paired with 2 or a backfill.
Whichever is chosen, all three backends must move together or this becomes a parity break again.
Split out of #313 so that issue can close with #319. This is the half of #313 that is not a defect in the code as written — it is an open product decision.
Why it is not a regression
#313 framed the method filter as having "silently become case-sensitive" in
c7a5dcae. That is not what happened:89a3e4dc)LIKE 'GET %'— case-insensitivestartsWith(endpoint, 'GET ')— case-sensitivec7a5dcaeSUBSTR(...) = 'GET '— case-sensitiveThe ~24 hours between #290 and
c7a5dcaewere the parity break;c7a5dcaeclosed it by aligning the embedded backends onto ClickHouse. It also shippedTestEndpointRepository_FindGroupedByEndpoint_MethodFilter, which insertsget /api/lowercaseand asserts it is excluded — the exclusion is deliberate and tested.SUBSTRoverLIKEis independently worth keeping:methodFilteris user input andLIKEwould let%and_through as wildcards.The actual problem
Lowercase-stored methods cannot be reached by any filter option.
getHTTPEndpoint(backend/app/controllers/otelcontrollers/trace_converter.go:462) concatenateshttp.request.methodverbatim with noToUpper:So
get /api/ordersrows really can exist. MeanwhileMETHOD_OPTIONS(frontend/src/lib/components/ui/endpoint-filter/methods.ts) offers only the 7 canonical values, whichnormalizeMethodFilteruppercases. No dropdown selection can ever match such a row — it is visible in the unfiltered list and vanishes under every filter.Options
get /xandGET /xas two separate endpoints, so the filter treating them as distinct is internally consistent.UPPER(SUBSTR(...)) = :methodon the embedded backends,startsWith(upper(...), ?)on ClickHouse. Keeps the no-wildcard property, matches howsearchalready behaves in the same clause builder (INSTR(LOWER(...), LOWER(:search))), and reaches rows already stored. Costs the deliberate test above.ToUpperthe method ingetHTTPEndpoint. Fixes the cause rather than compensating at query time in three SQL dialects, but does not reach rows already stored, and merges two currently-distinct endpoint groups.Option 3 is the one I'd pick — it addresses the source — with the caveat that existing rows stay unreachable unless paired with 2 or a backfill.
Whichever is chosen, all three backends must move together or this becomes a parity break again.