Fixes#3764
Supersedes #13195 (stale base, unsigned CLA, and non-rendering — see
Units below). The event-parsing and exception-detection approach from
that PR is reused, credited via `Co-authored-by`.
## What this adds
- **Trace waterfall** — a marker per span event, positioned against the
trace window alongside the span bar. Clicking one selects its span.
- **Sidebar mini-timeline** — the same events re-plotted against the
*span's own* duration, above the Events tab. Answers "where within the
span did this fire?", which the waterfall can't show for short spans and
the table only shows as absolute timestamps. Clicking a marker expands
the matching row.
- **Exceptions stand out** — OTel semconv detection (`exception` name or
any flattened `exception.*` attribute) renders in the error token; other
events in amber. Both are existing registered tokens, so theming is
automatic and no new tokens were added.
Issue #3764 asks for "dots for events **and** exceptions", so all events
are marked with exceptions emphasised, rather than filtering to
exceptions only.
## Units — why the previous attempt rendered nothing
A span event's `_timestamp` is raw OTLP `time_unix_nano`
(**nanoseconds**), stored unconverted:
Frontend span timing is **microseconds**. The same field name therefore
means different units on a span than on its events. #13195 compared the
two directly, so every marker landed ~1000x outside the window and was
silently dropped by the range filter — matching the "I couldn't see the
UI changes" report on that PR. The `ns → µs` conversion is now confined
to one composable and pinned by a regression test using nanosecond
fixtures.
Corroboration that events are nanoseconds: the sidebar's own events
table already divides the same field by `1e6` to render milliseconds.
## Structure
`web/src/composables/traces/useSpanEvents.ts` owns event semantics —
parse (array or JSON string), convert, classify — while each caller
supplies its own window. The two surfaces differ only in that window.
Events with no usable timestamp, or outside the window, are skipped
rather than pinned to an invented position.
## Verification
- `npm run type-check` — clean
- ESLint on changed files — 0 errors, 0 warnings
- `prettier --check` — conforms
- Vitest — **184 passed**: 27 new (`useSpanEvents.spec.ts`), 7 new
(`SpanBlock.spec.ts`), 7 new (`TraceDetailsSidebar.spec.ts`), rest
pre-existing
Pre-existing and untouched: three `no-dupe-keys` errors in
`eslint.config.js`, and a `vue/no-dupe-keys` warning for `activeTab` in
`TraceDetailsSidebar.vue` (confirmed present on unmodified `main`).
## Reviewer notes
- **Not yet validated against a live ingested trace.** That step is what
would have caught #13195, and it is the one gap here.
- Specifically worth checking: `gen_ai.*` events are constructed with
`_timestamp: 0` in the OTel processor's test fixtures. If real SDK data
does the same, LLM spans will show no markers (dropped rather than
mispositioned, by design). If that turns out to be real, the follow-up
is an explicit "N events without timestamps" affordance, not a
fabricated position.
- Marker clustering for very dense event sets is deliberately deferred
until real traces show it is needed.
---------
Co-authored-by: coyaSONG <66289470+coyaSONG@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## Summary
- move the quick-mode field-list rewrite out of `Index.vue` into
`replaceSelectFieldList`
- return the query untouched when it nests a SELECT, instead of
corrupting it
- mask string literals and comments before matching, so neither can be
rewritten or hide a subquery
- add `quickModeFieldList.spec.ts` covering the reported query and the
literal/comment cases
Fixes#8542.
## Root cause
Enabling quick mode replaced the search bar's SELECT list via
`query.replace(/SELECT\s+(.*?)\s+FROM/gi, ...)`. The global flag applied
that to *every* `SELECT ... FROM` pair, so a subquery was rewritten as
readily as the outer query, and `(.*?)` swallowed the projection
including any `DISTINCT`:
```sql
where trace_id in (select distinct trace_id from trace_list_index ...)
where trace_id in (SELECT trace_id,_timestamp FROM trace_list_index ...)
```
The `IN (...)` is left comparing against two columns and the `DISTINCT`
is gone.
Dropping the global flag is **not** enough: `.` does not match a
newline, so in a query formatted over several lines the outer pair never
matches and the subquery is the *first* match, not the second. The
rewrite has to bail out instead.
The check and the rewrite both run over a masked copy of the query, so
`msg = 'select id from users'` is not rewritten and `in (/* c */ select
...)` cannot hide a subquery. Masking is a single pass because a quote
can appear inside a comment and a `--` inside a quoted value. The global
flag is kept — a multi-stream search is a UNION of one SELECT per stream
and each needs the field list.
`quick_mode` still goes out on the request, so field trimming for
`SELECT *` is unaffected. When the query is skipped the toggle stays
silent rather than notifying — happy to add a message if you'd prefer
one.
## Testing
- `npx vitest run src/utils/query/quickModeFieldList.spec.ts` — 18
passing
- mutation-tested: removing the subquery guard turns 7 red, removing the
masking turns 5 red
- `src/plugins/logs/Index.spec.ts` — 65 passing, unchanged
- `npm run type-check:app`, `lint:ci`, `format:check` — clean
Two nearby things I noticed but did not touch: disabling quick mode does
not restore the previous query, and the field list joins without a space
after the comma.