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>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Problem
Four defects around traces and the trace↔log correlation flows:
1. **Trace Graph is clipped.** The service map renders inside a 200px
band, vertically centred in an otherwise empty pane, instead of using
the available height.
2. **A span row's View Logs goes nowhere.** In OSS builds it navigates
to `/logs?stream=&from=NaN&to=NaN`; the logs page drops the filter and
falls back to its default window, so the user gets an unfiltered result
set instead of that span's logs.
3. **The log detail drawer has no View Trace button.** Opening the trace
for a log record only works from the inline expanded row.
4. **View Trace lived inside the JSON tab**, so it disappeared when
switching to Table.
## Root causes
**Trace Graph.** The ancestor chain was already correct — the sibling
Waterfall tab fills the same box. Only the leaf was clamped: the chart
carried `h-50! min-h-50!` (12.5rem, `!important`), which beat its own
root `h-full w-full`. That clamp dates to when the map lived in a small
collapsible timeline strip, where 200px was the intended design. When it
was promoted to a full-height tab, the scoped rule `.trace-details
.trace-chart-height { height: 12.5rem }` was left behind and its
two-class specificity silently defeated the `h-full!` added at the time.
The later design-token migration deleted the rule but preserved the
rendered result by translating it into utilities, which made the clamp
look deliberate.
**Span View Logs.** `buildQueryDetails()` reads the raw API shape
(`start_time`/`end_time`, nanoseconds), but the waterfall passes
formatted nodes, which `getFormattedSpan()` rebuilds as a fresh object
literal — camelCase, times as `startTimeUs`/`endTimeUs` in microseconds,
snake_case originals dropped. Reading `start_time` off one yields
`undefined`, hence `NaN`. The helper already had a `spanId || span_id`
fallback for the id, which is why the query string was correct while the
window was not.
**Drawer View Trace.** `JsonPreview` decides whether to offer the action
once, in `onBeforeMount`. `DetailTable` fills `rowData` in an `async
created()` — the `await` defers assignment past the child's mount even
though the work is synchronous — so the preview mounted with an empty
record, the check failed, the traces streams were never fetched, and
nothing recomputed it.
## Changes
- Trace Graph chart uses `h-full`; the orphaned `trace-chart-height`
class is dropped (its rule no longer exists) and two nested wrappers
collapse into one. Matches the full-bleed pattern in `ServiceGraph.vue`.
- The span row resolves the raw span from `spanMap` before calling the
helper — the same lookup this component already does for span data, HTTP
status and events — rather than teaching the shared helper to accept two
shapes. Guarded, since a span missing `span_id` gets a synthetic id
absent from the map.
- The span row also warns instead of navigating when no logs stream is
selected. The header's own View Logs button is disabled in that state
but the row-level one is not, so it was a silent dead end.
- `JsonPreview` re-evaluates the View Trace gate when the previewed
record changes, so the flag tracks its input rather than a single
snapshot.
- The action moves to the drawer's header row beside the wrap toggle,
reachable from every tab, with `hide-view-trace` on `JsonPreview`
(mirroring the existing `hide-view-related`). The gate, stream fetch and
filter move into `useViewTraceAction` rather than being copied into
`DetailTable` — `service_streams_enabled` is already hand-rolled in
three files and drifted. `JsonPreview` keeps exposing the same names, so
its specs are untouched.
Two latent issues surfaced while moving the action:
- `hiddenMenus` is seeded as an array and only becomes a `Set` once
`MainLayout` has read `custom_hide_menus`, so `.has()` is not always
available. Both shapes are now accepted.
- `OSelect`'s root sets `w-full`, which won a plain `w-40` and pushed
the button outside the drawer — the same class-collision shape as defect
1. Pinned with `w-40!`.
## Tests
No behaviour is asserted by new specs; existing suites cover the touched
paths and pass unmodified:
- `JsonPreview.spec.ts` — 91 passed (unchanged, including the assertions
on `tracesStreams`)
- `DetailTable.spec.ts` — 70 passed, 3 skipped
- `TraceTree.spec.ts`, `useTraces.spec.ts`,
`TraceDetailsSidebar.spec.ts` — 297 passed
- `TraceDetails` suites — 234 passed
- `SearchResult.viewTrace.spec.ts`,
`CorrelatedLogsTable.viewTrace.spec.ts` — passed
Prettier and ESLint are clean on every touched file.
## Verification
Checked against live trace and log data sharing a trace id:
- Trace Graph fills its pane (measured 549px where it previously
measured 200px), survives a tab round-trip, and reflows on window
resize.
- A span row's View Logs lands on `/logs` with the span+trace filter and
a ±60s window whose bounds match the span's real timestamps exactly;
with no stream selected it warns and does not navigate.
- The drawer's header shows the picker and button on both the JSON and
Table tabs with no overflow, and opens the trace with the correct id and
a ±15 min window.
- Console clean across every navigation.
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## What
Makes the evidence a synthetic browser check captures readable **where
you already are**, instead of one tab away.
Two related changes on the run-detail view:
**1. An evidence row opens to its full record.** The table renders three
fields the probe already captures and nothing displayed: the stack trace
on a page error, the resource type, and the first-party flag. Plus the
full URL — the row strips the host and then truncates, so a login URL
died mid-query-string with no way to see the rest.
**2. The step expansion's "Page activity" card now hosts that same
table.** It used to show five severity-ranked rows and a `5 of 136`
count — a card whose headline metric was its own incompleteness. It now
renders the step's whole bucket with the same header, sorting, row
expansion, filters, wrap and paging the Evidence tab has.
Supporting work: the filter row is extracted into one component shared
by both surfaces, so they cannot drift onto different controls; and the
Evidence tab's step scope is a select you can change rather than a
banner you can only dismiss.
## Why
Every question past "was there anything?" cost a tab switch, and the
correlation you arrived with — this step, this failure — had to be
rebuilt by hand against a step filter.
## Notable fixes found along the way
- **The Step column was sortable but sorted nothing.** It had no
accessor, so the comparator read a field the row does not have, compared
`undefined` to `undefined`, and left every row in place while the header
showed a working arrow.
- **The step expansion's content column had no width clamp.** As a flex
item it kept its content's intrinsic width, so a long URL pushed it past
the container and every card in it overflowed — and `truncate` and
`overflow-x-auto` both went inert, since each needs a bounded parent.
Measured at 1487px inside a 1000px container.
- **Expand chevrons did not line up.** The row status rail's class also
insets the first cell, so rows without a rail sat half a rem left.
Benign rows now reserve the slot transparently.
- **"Took" was never right-aligned** — the class sat on an inline span,
where it aligns nothing.
## Testing
`components/synthetics/`, `views/synthetics/`,
`composables/synthetics/`, `utils/synthetics/` — **1398 passing**.
`type-check`, `lint:design:strict` and `format:check` all clean.
New behavioural coverage where it was thin: sorting is driven through
the header and asserted on rendered row order rather than on column
config, which is what let the non-functional Step sort ship.
## Reviewer notes
- `EvidenceEvents.vue` is shared by both surfaces. The only differences
left are the Step column and the page size — everything else is
deliberately identical.
- The step select is gated on being passed options, which is how it
stays off the step card, where the step is already the scope. A test on
each side pins that.
- The per-step card's five-row cap is deliberately removed; pagination
bounds the height instead, which bounds the card *and* keeps every event
reachable in place.
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>