Addresses the mechanism half of #13966 — auto-translations were
context-blind because `translate_batch()` sent bare string arrays with a
one-line system prompt.
## What changed
- **Key-path context**: each string is now sent as `{"key":
"alerts.incidents.statusOpen", "text": "Open"}` and the prompt tells the
model to use the key to disambiguate ("Open" there is an incident
status, not the verb). Keys are context only and never echoed into the
output; the response format is unchanged.
- **Product background + `TERMS` glossary** (`translator.py`): the
prompt now introduces OpenObserve and carries one term table — an
English `sense` line per ambiguous term that steers all 14 languages at
once (an "event" is a record, not a party; a "stream" is a data stream,
not a brook; "live" is real-time, not a broadcast), plus optional
per-locale pinned renderings (zh-CN/zh-TW seeded: event → 事件, stream →
数据流, live → 实时, trace → 追踪, report → 报表, …).
- **Module vocabulary mined from the locale itself**:
`build_module_glossary()` extracts the committed `menu.*` translations
(Alerts → 告警, Pipelines → 流水线, Dashboards → 仪表盘) and merges them with
the pinned terms into a single conflict-free renderings table — pinned
terms win, so the prompt can never mandate two renderings for one word.
Works for every locale with no hand-maintained tables.
- The system prompt is built once per locale and is byte-stable (sorted
renderings), so DeepSeek's prefix caching keeps the added prompt tokens
cheap. Change detection, validation, batching/split-retry, and the state
file are untouched.
## Testing
- Stubbed the API client and verified: payload shape, batch/index
alignment (string + array leaves), failed-item isolation via split
retry, prompt assembly for locales with and without pinned terms,
byte-stability of the prompt, and the pinned-vs-module dedup (`Streams`
module entry absorbed by pinned `stream`).
- `python3 main.py --check` (the pure comparison the merge-queue gate
runs) is unaffected.
## Not in this PR
Existing wrong translations are not re-sent (state hashes unchanged);
the backfill pass and the bootstrap-run gap are tracked as follow-ups in
#13966.
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>
Design At: designs/dashboards/interactive-table-cell-drilldown
What changed
New component: DashboardLogDrawer.vue
Clicking a cell in a dashboard table panel now opens a side drawer
scoped to that field/value filter. The drawer shows filtered log events
with a SQL editor (Ctrl+Enter to run, Reset to restore default),
server-side pagination with configurable page size (50/100/200/500), and
an event detail sub-drawer on row click.
Event detail has three tabs:
Insights — Field Anomaly Profile (rarity scoring vs current page), Event
Timeline (SVG histogram of event frequency across the selected time
range), Log Patterns (grouped by structure via /patterns/extract, each
with "Open in Logs" that prefills the log explorer), and Surrounding
Events (events from the same stream in a configurable ±1–10 min window,
paginated 20/page with per-row expand)
Details — key/value field list with copy buttons
JSON — raw event JSON
Modified: PanelSchemaRenderer.vue
Fixed type mismatch: route.query.cell_panel returns string; panel schema
IDs may be number — comparison now uses String() on both sides
Added cell_event_ts to the URL param cleanup list so it's cleared when
the outer log drawer closes
Modified: en-US.json — added complete panel.logExplorer key tree
including the new insights subsection
Modified: design-debt-baseline.json — updated baseline after audit (new
file's rawVarInComponent uses are all in sanctioned color-mix()
positions)
URL persistence
All drawer state is encoded in URL params (cell_panel, cell_field,
cell_value, cell_stream, cell_stype, cell_t0, cell_t1, cell_event_ts).
Sharing the URL reopens both drawers and restores the exact event — on
mount it does a WHERE _timestamp = {ts} targeted fetch (size 1) rather
than searching through paginated results.
Zero backend changes
Uses only existing endpoints: search (SQL), search (histogram via
histogram() function), and patterns/extract.
---------
Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
Co-authored-by: ktx-abhay <abhay.padamani@kiara.tech>
Co-authored-by: Abhay Padamani <141122205+ktx-abhay@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>