Commit Graph
3185 Commits
Author SHA1 Message Date
Hengfei Yangandgithub-actions[bot] 94a0a58866 fix(i18n): backfill mistranslated locale keys via context-aware regeneration (#13968)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-26 13:29:11 +00:00
Bhargav a108194a7e fix(i18n): correct Spanish product terminology (Logs, Trazas, Spans, … (#13973) 2026-08-26 12:19:53 +00:00
e48d9b2b39 feat(traces): annotate span events and exceptions on the trace timeline (#13715)
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>
2026-08-26 10:12:35 +00:00
Hengfei Yang ebdc2f0a4e fix(search): release the admission slot and follower task status on cancellation (#13954)
Paired with o2-enterprise `hf/issue-2491-admission-slot-leak` (same
branch name).

**Stacked on #13953** — base is that branch, so the diff here is just
this change.
Retargets to `main` automatically once #13953 merges.

## Problem

#13953 fixed the wal file locks. `do_get` touches two more trace-id
keyed
resources, and both had the identical defect: release depended on
control flow
reaching a call, inside the window where gRPC cancellation drops the
future.

**The node's admission slot.** Released from three hand-written sites —
the two
error arms in `do_get` and `FlightEncoderStream::drop`. All three are
skipped
when the request is cancelled during plan generation, because the
encoder
stream does not exist yet.

**The follower task status.** In the super-cluster path the entry was
inserted
before plan generation and removed after it:

```rust
if is_super_cluster && !SEARCH_SERVER.contain_key(&trace_id).await {
    SEARCH_SERVER.insert(trace_id.clone(), TaskStatus::new_follower(vec![], false)).await;
}
let result = get_ctx_and_physical_plan(&trace_id, &req).await;   // <-- cancellable
if is_super_cluster && !SEARCH_SERVER.is_leader(&trace_id).await {
    SEARCH_SERVER.remove(&trace_id, false).await;
}
```

Cancel in the middle and the entry stays in the map for the life of the
process. Nothing else removes it: an explicit `cancel_query` would, but
a
timeout or a client disconnect would not.

## Approach

Both move onto the same ownership chain #13953 established.

`SessionGuard` — already armed on the first line of `do_get` and already
carried into `FlightEncoderStream` — now releases the admission slot
too, so
the three call sites collapse into its drop.

The follower task status is registered through an `AsyncDefer` that
lives in
the same scope as plan generation, so it is removed on every exit
including a
cancelled one. It is now removed only when this node registered it,
rather than
whenever the entry is not a leader's: a request can no longer clear an
entry it
did not create. In practice these coincide, since the composite
`{trace_id}-{job_id}`
is unique per request.

### Drive-by

The `physical plan generate error` and `physical plan execution error`
logs sat
*inside* `if get_o2_config().work_group.max_nodes_per_query > 0`, so a
plan
failure was silent unless work groups happened to be configured — and
silent in
every OSS build. They are unconditional now.

## Enterprise side

The paired PR gives `Running` reservations a TTL backstop. They had no
expiry
edge at all, so a slot whose release was lost — this cancellation path,
or a
leader that dies before sending `Release` — was held until the process
restarted. This PR closes the local path; the backstop covers the remote
one,
which no local guard can.

## Verification

- `cargo fmt --all` and the CI clippy command: clean
- Enterprise verified via the `Cargo.toml.openobserve` swap against the
paired
  branch: `cargo check --all-targets` clean, and `cargo clippy --no-deps
-p openobserve-api-grpc --all-targets` clean. The workspace-wide
enterprise
clippy still reports the two pre-existing `cognitive_complexity` errors
in
`core/src/alerts/scheduler/handlers.rs` and `core/src/ofga/mod.rs`,
untouched
  by this change
- `api-grpc` flight tests 21/21

## Also here

Three zh-CN strings whose English is *events* said 活动 (activity).
Unrelated to
the rest of the PR, carried along at review request.

| key | en-US | was | now |
|---|---|---|---|
| `home.docsCountLbl` | Events | 活动 | 事件 |
| `rum.searchEvents` | Search Events | 搜索活动 | 搜索事件 |
| `logs.noEvents.title` | No events found | 未找到任何活动 | 未找到任何事件 |

zh-TW already had 事件 for all three, and every other locale translates
them as
events. `logs.noEvents` also contradicted itself — `descNoFilters`
directly
below the title already said 未找到任何事件.

## Not in this PR

The `QueryManager` map has a `created_at` on every entry but no reaper.
With
this change its entries are removed on cancellation, so it is no longer
known
to grow — but it has no backstop the way the ledger now does.
2026-08-26 09:09:35 +00:00
b09b56e25a feat(llm): add Phase 3.0 experiment workflows (#13778)
## Summary

- allow dataset rows without an expected output across management APIs
and UI flows
- add immutable experiment preview, creation, listing, persistence,
RBAC, and super-cluster synchronization
- add Experiment as an evaluation target with slot hydration, scoring,
and result indexing
- harden idempotent creation, scorer-version pinning, score-index setup,
and upgrade migrations

## Tickets

- Related to
[openobserve/o2-enterprise#2398](https://github.com/openobserve/o2-enterprise/issues/2398)
- Related to
[openobserve/o2-enterprise#2399](https://github.com/openobserve/o2-enterprise/issues/2399)
- Related to
[openobserve/o2-enterprise#2400](https://github.com/openobserve/o2-enterprise/issues/2400)

## Paired Enterprise PR

-
[openobserve/o2-enterprise#2413](https://github.com/openobserve/o2-enterprise/pull/2413)

## Validation

- focused Rust and frontend tests for the affected dataset and
experiment seams
- experiment migration test passes
- upgraded local environment applies schema version 68 and returns 200
from GET /api/team3/experiments

---------

Co-authored-by: sai nikhil kethe <nikhil@openobserve.ai>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-26 05:47:37 +00:00
Omkar Kesarkhane 2c17c07a11 fix(rum): restore RUM-traces correlation for zero-stripped trace ids (#13959)
## Problem

RUM-to-traces correlation stopped working after the browser SDK 0.4.x
line. The SDK's identifiers became plain `BigInt`, so the unchanged
`toString(16)` serialization silently started dropping leading zeros.
Trace ids are UUIDv7-shaped (48-bit ms timestamp in the top bits), so
the leading hex nibble is always zero today — effectively every RUM
event stored a 31-char `_oo_trace_id`/`_o2_trace_id` while the traces
stream stores the padded 32-char `trace_id` (the `traceparent` header
was padded correctly, so backend traces have the full id). Exact-match
correlation could never hit, in either direction.

Verified against live data: for sampled traced requests, `'0' +
stored_id` found the backend trace every time; the stored id never did.

## Fix (frontend compatibility layer)

Works for the already-ingested short ids **and** for correct ids from
the fixed SDK:

- `utils/rum/fields.ts`: `normalizeTraceId` (pads to canonical 32-char
hex), `traceIdLookupVariants` (padded + legacy zero-stripped forms),
`rumFieldEqualsAnySql` (matches both namespace spellings x both id
forms), and a shared `RUM_CORRELATION_TRACES_STREAM` constant replacing
four scattered "default" literals.
- `useRumSpanBuilder`: trace→RUM lookup queries both id forms; canonical
id stamped on generated RUM spans.
- `useTraceCorrelation`: backend-span query now targets the real
`default` traces stream (was a nonexistent `_traces` stream) as
`page_type: "traces"` (was `"logs"`), with the padded id.
- `EventDetailDrawerContent`: trace navigation pads the id before
building the route.
- `PlayerTracesTab`: the session Traces tab canonicalizes ids before the
traces-stream metadata join (rows were silently dropped).
- `ErrorViewer`: error correlation card receives the padded id;
implausible (non-hex) ids hide the card instead of showing a misleading
empty state.
- `package.json`: browser SDK bumped to 0.4.2-beta.3, which serializes
trace/span ids as fixed-width canonical hex at the source (resolved
`browser-rum-core` verified at 0.4.2-beta.3).

## Testing

- New unit coverage: `fields.spec.ts` (16 tests) plus regression tests
in each touched spec (legacy 31-char id fixtures asserting padded
32-char behavior); full RUM/traces suites green (53 files, 1971 passed).
- Live-verified against real data: a trace from the broken window
renders the full RUM hierarchy (Session→View→Action→Resource + backend
spans) where the old code showed backend spans only; pre-regression
sessions still correlate (no behavior change for correct ids).
- `npm run lint` and `npm run type-check` clean.
2026-08-26 04:24:16 +00:00
91e524ce2d feat(dashboards): interactive table cells with in-place Logs Explorer drawer (#13675)
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>
2026-08-25 10:31:13 +00:00
abc0fdc42d feat: model pricing time-based conditonal pricing (#13903)
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>
2026-08-25 07:04:58 +00:00
Amey Pawar 9a44af980e fix(logs): stop quick mode rewriting SELECT inside subqueries (#13787)
## 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.
2026-08-25 05:05:23 +00:00
Ashish Kolhe b2639c4f9f feat(dbm): Database Monitoring (#13918)
## What this is

Database Monitoring: an aggregation and presentation layer over
telemetry OpenObserve already ingests. It reads three vantages — client
spans, server-vantage logs, and collector metrics — and turns them into
per-statement, per-instance and per-database views.

No new storage engine and no new ingest path. Statements are normalized
and rolled up into a stats stream, and the read API queries that stream
through the existing search path.

## Shape of the change

| | |
|---|---|
| Read endpoints | 17 |
| UI pages | 8 (plus a shell) |
| Commits | 141 |

**Backend** — `src/core/src/db_monitoring/` holds the non-HTTP half
(normalization, rollup, server-vantage parsing). The read API lives in
`src/api/management/src/request/db_monitoring/` as a handler / models /
service trio; the service layer is split one file per feature.

**Frontend** — `web/src/views/DatabaseMonitoring/`, with shared logic in
`web/src/composables/dbm/`.

## Feature availability

Databases, queries, query detail and history, endpoints, samples,
activity, insights, plans, server metrics, server queries, server
samples, instances, instance metrics, badges — is available in OSS.

## Reviewing this

It is a large diff, and most of it is additive: 327 files, and the two
largest non-test files are the shared service module and the deadlocks
service. Suggested order:

1. `src/api/management/src/request/db_monitoring/handler.rs` — the 17
entry points and their gating
2. `src/api/management/src/request/db_monitoring/service/common.rs` —
auth, SQL building, the shared read path
3. `src/core/src/db_monitoring/rollup.rs` — how the stats stream is
produced
4. `web/src/views/DatabaseMonitoring/` — the pages

Time semantics are the subtlest part. Rollup rows are stamped at the
window **end**, so reads are exclusive at both edges; `stats_read_range`
carries the reasoning and a test pins it.

## Known follow-ups

- **The server-vantage stream named as `_o2_dbm_server`.** 

## Testing

- Rust: unit tests across the DBM core and read service, including
SQL-building, auth-gating and normalizer cases
- Web: unit tests across the DBM pages and composables
- Playwright: DBM suite covering the pages and cross-page navigation

Verified before opening: OSS and enterprise builds both compile
warning-free, `cargo fmt` clean, `vue-tsc` clean, and the full web unit
suite passes (44,704 tests).
2026-08-24 11:08:00 +00:00
5f034c6666 feat: folder icons (#13895)
Design at:
https://github.com/openobserve/designs/blob/main/ux/folder-icons/design.md

---------

Co-authored-by: Kirtan Thakkar <kirtan@kiara.tech>
Co-authored-by: ktx-abhay <abhay.padamani@kiara.tech>
Co-authored-by: Abhay Padamani <141122205+ktx-abhay@users.noreply.github.com>
2026-08-24 08:56:02 +00:00
Prabhat Sharma fea71e02d4 feat(alerts): alert library — browse, preview and install curated alerts (#13922)
## Alert Library

A curated catalog at `/alert-library`: browse packs of ready-made
alerts, see
which ones can actually run against the streams this org already
receives,
preview a verdict against real data, and install one or many.

The library is a **peer page** of Alerts / Notification Destinations /
Templates
— flat routing, presented as tabs via a shared `AlertSectionTabs` in
`OPageLayout #header-tabs` (the pattern `PipelineSectionTabs` already
ships).

### What landed, by phase

| Phase | What |
| --- | --- |
| Data layer | S3 manifest fetch, cache, readiness resolution
(`useAlertLibrary`, `constants/alertLibrary`, `types/alertLibrary`) |
| Gallery | Rail (packs / categories, multi-select, segmented switch),
stat-strip availability facets, cards |
| Drawer | Detail view with query preview and customizable tunables |
| Install | Bulk-install wizard — destination selection, per-alert
tunables, stale-session hardening |
| Prefill | Carry a library alert (query, trigger, threshold, severity)
into the alert editor |

### Design decisions worth knowing

- **Rail navigates, strip filters.** Availability is deliberately not in
the rail
  — carrying both overflowed it and pushed a facet group below the fold.
- **Colour marks the exception only.** No "Ready" pill (it would
describe ~75 of
  86 cards and signal nothing); only unusable cards carry a label.
- **A failed `/streams` call leaves readiness unknown**, rather than
claiming
"none of your data matches" — that claim would be about the request, not
the
org, and it is the most alarming thing this page can say. Counts read
`—`
until there is an answer, and a failed *refresh* keeps the verdict it
already had.
- **No "Installed" tile** — installed detection doesn't exist yet, and a
tile
  reading a permanent zero is dishonest UI.
- **`severity: warning` renders as `medium`** — the badge group's scale
is
critical/high/medium/low/info, and medium is the amber it also gives P3,
which
  is exactly the priority a warning alert installs as.
- Pack and category names come from the manifest, so they render as
`raw()` data —
  a pack added upstream needs no frontend release.

### Design-system changes (used by the library, applied app-wide)

- **`warning-quiet` badge tone** — one variant whose values differ by
theme: the
soft amber wash in light, a neutral chip in dark. Neither the filled
amber chip
nor the neutral one was right in both, because the two ramps aren't
symmetric.
- **Border tokens now meet WCAG 2.1 SC 1.4.11.** Every border token
measured
under 3:1 (`border-strong` was 2.52:1). `border-default` firms to
grey-300 and
  controls get a token that clears 3:1.
- Nav: Reliability's flyout groups its four alerting entries under one
**Alerts**
header, declared in the same order as the tab strip those pages carry.
Flyout
  levels are sized by depth.
- `sensors-off` icon added to the `OIcon` registry.

### Fixes that rode along

- **metrics**: a shared URL whose `metrics_data` blob omits panel query
fields no
longer blanks the whole metrics page — blob queries are filled out to
the panel
  schema before being laid over.
- **dashboards**: a PromQL series with no legend template was named
`JSON.stringify(metric)`, so fifty series read identically for the width
of the
chip. Series are now named by the labels that actually differ,
preferring the
ones that say *which workload* it is (namespace / service / pod / host).
Correlation's semantic groups refine that when loaded, but nothing is
fetched
to render a chart — that endpoint is enterprise-only and a chart must
render
  the same on OSS.
- **alerts**: preview evaluates against the alert's real trigger;
deleting a row
no longer reloads the list behind it; peer tabs no longer shift when
switching
  alerting pages.
- **ui**: badge-only select options no longer render bold.

### Tests

- `test(search)`: pins the streaming-aggregate **wire contract** at the
HTTP
boundary. Under streaming aggregates each partition carries the
cumulative
merged state (last frame is the answer) while ordinary partitions are
disjoint
and must be summed. No pytest had ever received `streaming_aggs: true` —
the
emitter could have flipped to per-partition deltas with the whole suite
green
while dashboards, Logs and the alert preview all silently over-counted.
- `test(e2e)`: four helpers that passed only while an org stayed small —
an
assertion reading past a 20-row page, an unfiltered `OSelect` that
virtualises
past 50 entries, a tab click landing on the rename pencil, and two
builder
  tests asserting on whichever metric sorted first.
- New unit specs across the library components, composable, facets,
tunables and
  install path.

### Notes

- **i18n**: en-US only; CI generates the other 14 locales.
- **No backend changes** — this branch is entirely `web/` plus test
helpers.
2026-08-23 20:32:28 +00:00
Ashish Kolhe 029fbbd3d5 fix: stop serving deployment internals on the unauthenticated /config (#13921)
## Problem

`GET /config` is unauthenticated by design

## Fix

Split the endpoint:

- **`GET /config` (unauthenticated)** now returns only the login-page
bootstrap: `sso_enabled`, `native_login_enabled`, the four custom-logo
branding fields, the `rum` init block, `telemetry_enabled`,
`build_type`, and `commit_hash` (kept so the UI's stale-build detection
keeps working; version, license, billing orgs, storage region, etc. no
longer leak). An **exact-key-set test** pins this surface so a new field
cannot land on the unauthenticated endpoint unreviewed.
- **`GET /api/{org_id}/config` (authenticated)** serves the full
configuration, minus `instance`, which is dropped entirely. It goes
through `auth_middleware` like every service route and is classified as
a querier route, matching where `/config` was served before.
- The web app fetches the bootstrap at boot and the full config from
MainLayout once an org is known (with a watcher for fresh sessions), and
the logo-settings / Usage-tab refresh paths use the authenticated
endpoint.

## Paired PR

`o2-enterprise` branch `fix/config-api-exposure` adds the
`ROUTE_PERMISSIONS` entry (`bypass: true`, like the marketplace routes)
so any authenticated org member can fetch `GET /{org}/config` past
OpenFGA.

## Verification

- Router tests: bootstrap exact-key-set, `/config/reload` still 401,
`/{org_id}/config` dispatch + no-`instance` (3/3)
- `cargo test` for config / api-http / api-management crates: green
- Targeted web unit tests (config service, buildVersionChecker, General
settings): green
- Enterprise build verified via the `Cargo.toml.openobserve` swap:
`cargo check --all-targets` clean
- `o2_openfga` route-resolution tests for the new entry: green
- `cargo fmt --check` clean; CI clippy (`--workspace --all-targets -- -D
warnings`) clean
2026-08-23 04:47:45 +00:00
ktx-vaidehi 7e0bdec6fd fix: png to svg chart types (#13911) 2026-08-21 06:54:16 +00:00
Omkar Kesarkhaneandgithub-actions[bot] fac24d59bb fix(traces): trace-log correlation fixes and Trace Graph height (#13901)
## 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>
2026-08-21 05:58:02 +00:00
Omkar Kesarkhaneandgithub-actions[bot] cb1b064a5d fix(synthetics): support to read a run's evidence without leaving the step (#13879)
## 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>
2026-08-20 07:05:05 +00:00
0f4b606e8e feat(synthetics): super-cluster support, then move synthetics to OSS (#13795)
> ### ⚠️ This PR carries four changes — review alongside
openobserve/o2-enterprise#2420
>
> 1. **Super-cluster support (consumer half)** — `8ceb041c43 …
5885c57b3a`.
> 2. **Synthetics OSS migration** — `3a380087a3 … 2596300dbf`, nine
commits — one per build-order step, plus a ninth for fixes found by
running the binary. Moves synthetics out of `o2_enterprise` into
`src/synthetics` (`openobserve-synthetics`), leaving only the
private-VPC-agent path enterprise: `job_api::lease`, `agent/*`, private
locations and the agent-setup install flow. Paired with
openobserve/o2-enterprise#2420.
>
> 3. **Last-check columns read from the results stream** — `6af411e808`,
`e7a3f2c759`. Outside the region whose scheduler runs a check, the list
showed a stale status and a blank Last check / response time:
`last_check_status` replicates only on change, `last_triggered_at` is a
runtime column that does not replicate, and `last_response_ms` was never
populated anywhere. The `synthetics_results` stream already carries all
three and is federated, so the list API reads it instead of replicating
the columns — no queue traffic, no schema change. Best-effort: any miss
keeps the stored DB values, so search being unavailable cannot take the
list down. OSS-only; no enterprise counterpart.
>
> 4. **Check secrets are readable in every region** — `d47a6b2345` (+
o2-enterprise `da71d7ec`). The per-org DEK is minted by each region for
itself, so a check created outside the scheduler region replicated fine
and then silently never ran — `resolve` could not decrypt its
credentials (o2-enterprise#2451). Secrets now cross the queue **in
clear** and the applier re-encrypts them under its own DEK on arrival,
fixing the class at the replication boundary with no key to distribute
or keep in sync.
>
> ⚠️ **This puts customer secrets in plaintext on the inter-region bus**
— a deliberate, recorded trade-off (`o2oi_` ingest tokens and probe
tokens already travel it in clear). Securing the bus itself (TLS on the
gateway links, short retention on secret-carrying subjects) is the real
fix and is **not** in this PR.
>
> ⚠️ **Deploy consumers before producers.** The payload's bytes did not
change, only the meaning of the secret strings, so an un-upgraded
consumer handed the new payload would write customer passwords to disk
in clear. The plaintext-carrying messages therefore use new variant
names (old ones retired as `CreateEncrypted`/`UpdateEncrypted`), so an
old region fails serde and redelivers until upgraded instead of silently
persisting plaintext.
>
> An OSS build can now create, schedule, dispatch to Lambda, take
`resolve`/`ack` and report a check. 7 393 lines moved into the new
crate; 679 lines stay enterprise.
>
> **🛑 Deployment prerequisite (2 of 2).** `ZO_SYNTHETICS_ENCRYPTION_KEY`
must be set to the **same** base64 64-byte value in every region of a
super cluster. Without it synthetics falls back to the per-org DEK and
checks carrying secrets silently fail to run outside the scheduler
region. It is read once at startup and pinned — changing it later
requires a restart, and changing it at all orphans every secret already
encrypted under the old value.
>
> **🛑 Deployment prerequisite (1 of 2).** 14 `O2_SYNTHETICS_*` env vars
are hard-renamed to `ZO_SYNTHETICS_*` with **no fallback read**, and
`ZO_SYNTHETICS_ENABLED` defaults to `false`. A deployment still setting
the old names does not lose tuning — **synthetics stops running
entirely, and the first symptom is checks silently not firing.** Update
env vars *before* rolling the binary.
>
> #### Things a reviewer should look at first
>
> - **`super_cluster_queue` does not depend on
`openobserve-synthetics`** (`cargo tree` → 0), which is what keeps the
applier's loop-prevention guarantee a property of the crate graph rather
than a convention. `a7d5fdd969` adds the assertion that fires if that
ever changes.
> - **`job_upload` was skipping authorization in OSS builds**
(`4c3a0b8f0d`). Unreachable before, because no synthetics route was
registered in OSS; registering it would have made it an unauthenticated
artifact upload. `authorize_probe` is ungated and the bypass is gone.
> - **The dependency edge had to reverse in a single commit**
(`3247d4093f`). Cargo rejects an `o2_enterprise` ↔ OSS package cycle at
the manifest level — `optional` and feature gating do not help — so the
two edges could never coexist, and everything needed for the reversal
lands together. This is why the spec's steps 4 and 5 are one commit and
the config moved earlier than planned.
>
> #### Accepted OSS limitations, all documented in the commits
>
> - No notifications: `notify_check_result` ends in
`alerts::alert::dispatch_notification`, which is enterprise. An OSS
build runs the check and records the run; it has nowhere to send an
alert, exactly as for alerts themselves.
> - No per-resource RBAC on synthetics, and no OpenFGA ownership tuples
— both are enterprise capabilities across the platform.
> - Private locations are refused with a message rather than created.
> - Agent-served public `net-*` pools are unavailable, so an OSS user
needs a Lambda for every public pool (spec §1).

---

Consumer half of synthetics super-cluster support. **Depends on
openobserve/o2-enterprise#2420** (producer, message types, queue struct)
— and **this side must deploy first**: `MessageType` bytes 83–85 hit a
panicking `unreachable!()` in `From<u8>` on a consumer that does not
know them, which takes down the subscriber task rather than failing one
message.

Based on `fix/lease-claimed-by-2270` (#13785), not `main`, so the diff
is only this work.

## What this fixes

Synthetics writes are not querier routes, so a create lands on ingesters
in whichever region received the request and commits to that region's
Postgres. There is no synthetics topic on the super-cluster queue, so a
check created in a region that runs no scheduler is never seen by the
region that does: the UI shows it enabled, the scheduler has never heard
of it, and nothing reports the gap.

## Commits

| commit | what |
| --- | --- |
| `8ceb041c43` | orphan-detection query + metrics |
| `1c3e10da48` | apply replicated check config |
| `79e6abc052` | apply replicated locations and probe tokens |
| `16d4fa0fde` | render unknown live status instead of a false pending |
| `5885c57b3a` | share the job-cluster predicate |

## Design notes worth reviewing

**Config columns only, and the list is not restated.**
`synthetics_checks::update` rebuilds its `ActiveModel` from the row
already in this region, and `update_mutable_fields` writes only
user-editable columns — so `next_run_at` and the alerting counters keep
local values for free. Hand-listing them in the consumer would duplicate
a list that already exists and let the two drift. A test asserts every
runtime column comes back `Unchanged`.

**`put()` is deliberately unused.** Its insert branch builds from
`build_active_model`, which ends in `..Default::default()` and never
assigns `owner`, `tz_offset` or `next_run_at` — a replicated create
through it would drop the owner and fire a scheduled-later check
immediately.

**`create()` gains `use_given_id`**, matching `table::alerts::create`.
It generated its own uuid, which would have made every region invent a
different primary key for the same check.

**Consumers apply through `infra::table` only.** The publish lives one
layer up in the enterprise service, and `infra` cannot reach the
enterprise crate — that layering is what stops a region re-broadcasting
what it just applied. `source_cluster` does not cover this: it only
stops a region re-consuming its *own* message. Each consumer greps its
own source to keep this true.

**Runtime tables are never replicated** — `synthetics_jobs`,
`synthetics_runs`, `synthetics_agents`, and the locations
`down_notified_at` claim. Asserted at both producer and consumer.

## Known gaps, named not hidden

- If two regions mint a token (or location) with the same name for one
org before either message crosses, the UNIQUE constraint rejects the
arriving row. Local row wins, logged loudly. Erroring would redeliver to
dead-letter, which is worse.
- Pre-existing and untouched:
`write_triggers_stream`/`write_results_stream` in the enterprise reaper
post to `_meta` with the *org's* ingest token, which 401s, and neither
checks response status. Orphan detection fixes this in its own path
only.
- Pre-existing: `create` never sets `tz_offset`, so a new check gets DB
default 0 until its first edit. Replicates identically everywhere, so it
causes no divergence.

## Spec corrections

The spec (`docs/synthetics-lcl/2026-08-11-p1-3-super-cluster-spec.md`,
local notes) is wrong in five places, all found by reading the tree:
§1's invariant evidence (see the enterprise PR), §10's never-run guard,
§4's column list (and it lists six runtime columns — there are seven,
`alerting` is missing), §3's "private rows read-only when remote", and
§7's claim that non-scheduler regions render OFFLINE (they render
`pending`).

## Verification

infra 42, super_cluster_queue 32, jobs 21, web 111/111 vitest +
type-check + design lint, `cargo fmt --check` and clippy clean, full
`--features enterprise` build.

Pre-existing failures on this branch, unrelated and untouched:
`super_cluster_queue::schemas::tests::test_parse_key_{valid,metrics}`,
`infra::table::ratelimit::tests::test_add_batch_missing_rule_id`, and 8
web vitest failures in `buildPayload.spec.ts`/`payloadContract.spec.ts`.

---------

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>
2026-08-20 05:40:34 +00:00
Omkar Kesarkhaneandgithub-actions[bot] d6c726c222 fix(synthetics): show where recorded steps will land (#13860)
## What

Shows where recorded steps will land in a browser journey — as a
labelled divider above the target row, previewed on hover and held for
the whole recording session.

## Why

Each step row carries a Record control meaning "restore the journey up
to here, then record new steps before this row". It was the most
expensive click in the editor — it opens an incognito window and replays
every preceding step — and nothing said where the result would land
until after that had already started.

The only signal was a "Recording here" badge rendered inline in the
anchored row's details cell, between the action icon and the action
label. Sitting inside the row, it read as *this row is recording* when
it means *new steps go above this row*.

## What changed

- The badge is replaced by a divider on the row's top edge: two rule
segments with a centred label between them, straddling the boundary so
it names a position **between** rows rather than a property of one.
- Hovering or focusing a row's Record control draws the same marker in a
lighter tone, so the destination is legible while the click is still
free.
- The preview is suppressed wherever the control is disabled — the first
row, a locked table, or an extension too old to restore. A preview of an
action you cannot take is worse than no preview.
- Focus is bound alongside hover, so the affordance is not mouse-only.

Two edge cases surfaced while testing and are fixed here:

- A replay started from the toolbar locks the table while the pointer
rests on a row control. Nothing moves, so no `mouseleave` arrives, and
the preview sat on a locked table offering a click that was now refused.
- Both render modes share the details cell slot, so an anchor id
reaching results mode drew a marker on a finished run — over a cell with
no positioning to hold it.

## Implementation notes

No `OTable` change. The marker is absolutely positioned against the
details `<td>`, which becomes a containing block via the column's
`meta.cellClass`. Two consequences worth knowing:

- **Nothing reflows.** Toggling the marker on hover repaints only. A
rule that took layout would jitter the whole table as the pointer
crossed the action column.
- **The gap is real, not painted.** The label sits between two `flex-1`
segments rather than over a continuous rule. That centres it without
measuring anything, and means it needs no background to avoid being
struck through.

`getCellStyle` lifts the cell's `overflow: hidden` on the marked row
only, so the label can overhang the boundary. Step-name truncation is
unaffected — that ellipsis lives on the inner wrapper, not the `<td>`.

## Testing

- `JourneySteps.spec.ts`: 43 tests (18 new), covering the committed
marker, the hover and focus preview, suppression on every disabled path,
tone precedence when a committed anchor and a hover coexist, anchor
reassignment, the positioning contract, and results mode.
- Full journey suite green: 336 passed, 1 skipped,
`BrowserJourney.spec.ts` unmodified — which is what shows the marker
rides the existing `anchorId` prop rather than duplicating state.
- Guards were mutation-checked: deleting each one turns the suite red,
so none of them pass vacuously.
- `lint`, `type-check`, `format:check` and
`check-design-consistency.mjs` all clean.

## Not covered

jsdom has no layout, so positioning is pinned by class assertions only —
this has not been verified visually. Worth an eye on the label at the
dense row height, in both themes.

Out of scope, and unchanged by this PR:

- The marker does not survive into recording — `BrowserJourney` replaces
the whole journey table with a capture-only table while recording, so
there is no row to mark.
- The toolbar Record button draws no marker below the last row.
- Downstream steps keep stale pass ticks after a splice, and the passed
banner recomputes its total from the new step count.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-20 05:21:09 +00:00
4b6491a0ea fix: session detail output message fixed (#13889)
## Summary

- web/src/plugins/traces/SessionDetails.vue
- web/src/plugins/traces/composables/useSessions.ts
- web/src/plugins/traces/threadView.utils.ts (new)
- web/src/plugins/traces/threadView.utils.spec.ts (new)

## Test plan

- [x] `npx vitest run` on `threadView.utils.spec.ts` - 114 passed
- [x] `vue-tsc --noEmit` (`type-check:app`) - clean

---------

Co-authored-by: sai nikhil kethe <nikhil@openobserve.ai>
Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
2026-08-20 04:23:06 +00:00
udev 84b666ce0b fix: annotation_queues item hydration intermittent 500s (#13881)
## Summary

RCA (2026-08-19, 08:20-08:22 UTC): `GET
/api/{org}/annotation_queues/{queue_id}/items/{item_id}` (and its
`/reviews` sibling) intermittently returned 500. Originally attributed
to search work-group admission capacity exhaustion; tracing the actual
code found a different root cause.

**Real mechanism**: `eval_jobs::search::execute()`'s registered
executors (in `src/jobs/src/job/mod.rs`) called
`search_service::search("", ...)` with a hardcoded empty `trace_id`.
With no propagated OTel span in that detached task,
`search_service::search()` falls back to OpenTelemetry's *invalid* trace
ID for every such call - the same `000...000` seen in every failing log
line. Every concurrent internal search therefore shared one
`QueryManager` map key: one query's cleanup (`SEARCH_SERVER.remove`)
deleted another's still in-flight registration, so the next
`insert_sender` failed with "trace_id not found" -> `SearchCancelQuery`
-> 500. `work_group_checking` never reached the real concurrency-wait
branch in this failure mode - "work-group capacity" was never the actual
mechanism.

## Changes (3 commits)

1. **`fix(annotation-queues): merge item detail and reviews into one
call`** - the Queue Workbench always fetches item detail + reviews
together (confirmed: no caller ever needs one without the other,
including after review submission); fold them into one backend call,
cutting the per-item load from 2 HTTP round trips to 1. See companion PR
openobserve/o2-enterprise#2473 for the response-shape change this
depends on.
2. **`fix(annotation-queues): map search admission cancellation to
429`** - `annotation_queue_error_response` unconditionally 500'd on
`AnnotationQueueError::Search`/`Hydration`, even when the underlying
cause already has a defined 429 `http_status()` (`SearchCancelQuery` /
`RatelimitExceeded`). Downcast and map correctly instead.
3. **`fix(search): stop colliding on trace_id in internal eval
searches`** - the actual root-cause fix: generate a real per-call
`trace_id` via `config::ider::generate_trace_id()` at all three internal
`search_service::search("", ...)` call sites instead of passing `""`.

## Dependency

This PR's enterprise build depends on openobserve/o2-enterprise#2473
(same branch name `ud/fix-annotation-queue-hydration-500s`) via the path
dependency in `Cargo.toml.openobserve` - that PR adds the `reviews`
field on `AnnotationQueueItemDetail` this PR's merge (commit 1)
consumes.

## Test plan

- [x] `cargo build --features enterprise`
- [x] `cargo test -p openobserve-api-management --lib --features
enterprise annotation_queue` - 11 passed (incl. 2 new tests for the 429
mapping), no regressions
- [x] `cargo test -p o2_enterprise --lib annotation_queues` (companion
repo) - 16 passed, no regressions
- [x] `cargo clippy --features enterprise -p openobserve-jobs -p
search_service -p openobserve-api-management --no-deps` - clean (no
warnings in touched files)
- [x] `cargo fmt --check` - clean
- [x] Frontend: `npx vitest run` on `llm-queues.service.spec.ts` +
`QueueWorkbenchPage.backTarget.spec.ts` - 9 passed
- [x] `vue-tsc --noEmit` (`type-check:app`) - clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-19 11:58:26 +00:00
Shrinath Rao be67ed442a fix(deps): add missing @noble/hashes@2.3.0 to web lockfile (npm 10 npm ci) (#13872)
## Problem

Every frontend build is red on `npm ci` in `web/`:

```
npm error `npm ci` can only install packages when your package.json and
          package-lock.json ... are in sync.
npm error Missing: @noble/hashes@2.3.0 from lock file
```

## Root cause (not a feature change)

- `web/package-lock.json` was last regenerated with **npm 11**, whose
resolver **omits** the explicit `@noble/hashes@2.3.0` entry that
`jsdom`'s transitive `@exodus/bytes@1.15.1` **peer-requires** (`^1.8.0
|| ^2.0.0`).
- The **Playwright build pins `node-version: 22` → npm 10**
(`.github/workflows/playwright.yml`). npm 10's `npm ci` recomputes the
ideal tree, needs that entry, finds it missing, and hard-fails.
- Reproduced locally: `npm@10 ci` **fails**, `npm@11 ci` **passes**, on
the exact committed lock. So it's a **toolchain mismatch**, independent
of any PR — it breaks `main` and every open PR building the frontend.

## Fix

Regenerated the lock with `npm@10 install --package-lock-only` → adds
the nested `node_modules/jsdom/node_modules/@noble/hashes@2.3.0` entry.
**+15 lines, lockfile only**, `lockfileVersion` unchanged (3).

Verified `npm ci` now passes under **both npm 10 and npm 11**.

## Note

The durable fix is aligning the Playwright build to **Node 24 / npm 11**
(matching the AI-review workflow and the Node 20/22 runner deprecation).
This PR is the minimal unblock; the Node bump can follow separately.
2026-08-19 09:47:28 +00:00
Chaitanya Sistlaandgithub-actions[bot] 351b356620 feat(alerts,slos): export as Terraform and OpenTofu (#13849)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-18 17:27:27 +00:00
Prabhat Sharmaandgithub-actions[bot] ecc1d5dbff fix(ui): clean up SLO detail and traces navigation (#13852)
## Summary

This PR combines the SLO detail cleanup and the Traces navigation fixes
into one change set.

### SLO cleanup

- remove the internal definition generation value from the SLO detail
title
- remove the unused translation key and update generated locale files
- normalize SLO health icon identifiers
- avoid rewriting unchanged locale files during translation updates

### Traces navigation

- add Spans as the first item in the Traces flyout
- route Spans, Traces, Service Graph, and Service Catalog through
/traces?tab=...
- redirect legacy Service Graph and Service Catalog paths while
preserving query state
- support same-route menu switching and retain stream, time-range, and
query context
- remove the deprecated search_mode parameter from generated and legacy
URLs
- keep Traces as the default for tabless URLs

## Validation

- focused Traces Vitest suites: 436 passed, 2 skipped
- app TypeScript check
- full frontend ESLint check
- Prettier checks
- live browser verification of Traces and Spans menu switching

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-18 17:07:57 +00:00
Hengfei Yang 7dd19d78ad fix: reduce eval match preview window to one hour (#13859)
## Summary

- reduce the new evaluation job matched-target preview from a rolling
24-hour window to one hour
- update span, trace, and session preview labels to show `last 1h`
- add deterministic coverage for the one-hour query range

## Why

Selecting a stream immediately runs a matched-target count query. The
hard-coded 24-hour window can scan a large amount of trace data on
high-volume streams even though this number is only a creation-time
preview.

## Impact

This only changes the matched-target preview query and its label. It
does not change the evaluation job schedule or execution range.

## Root cause

The preview composable and its labels were hard-coded to 24 hours.

## Validation

- `npm run test:unit -- --run
enterprise/components/onlineEvals/composables/useJobMatchedTargets.spec.ts
enterprise/components/onlineEvals/forms/job/JobPreviewPanel.spec.ts`
- `npm run type-check:app`
- focused ESLint and Prettier checks
- `npm run lint:design:strict`

## Companion PR

- openobserve/o2-enterprise#2452 fixes the OpenAI token-limit parameter
and raises the LLM judge defaults.
2026-08-18 13:39:27 +00:00
ktx-vaidehi a6c910f015 fix(web): logs scroll jump, dashboard tab-rename width, and banner usage (#13858)
Three unrelated UI fixes.

### `fix: logs page wrap scroll`
The Logs view delegates its scroll element to a container that also
holds the histogram. TanStack Virtual compensates `scrollTop` whenever a
measured row differs from its estimate — correct for a self-contained
list, wrong for a shared scroller. With wrap on, rows were remeasured on
every query refresh and the deltas pushed the whole page downward.

Adds `preserveScrollOffsetOnRowResize` to `useTableVirtualization`,
which pins `shouldAdjustScrollPositionOnItemSizeChange` to `() =>
false`. `OTable` opts in only when it has both a delegated `scrollEl`
and dynamic row height, so self-contained tables keep the existing
behaviour.

### `fix: dashboard tab rename size issue`
The rename input's auto-sizer grid track was `minmax(0, max-content)`.
Once the tab strip overflowed, the `0` minimum collapsed the track to
`0px` and the input vanished. Now a bare `max-content`.

### `fix: ingestion and webinar obanner usage`
Replaces two hand-rolled banner `div`s (`Ingestion.vue`,
`WebinarBanner.vue`) with `OBanner`. To make that possible without
appearance overrides, `OBanner` gains:
- a `promo` variant (the existing marketing gold, so the standalone
webinar bar still matches);
- `center`, which groups message and actions in the middle of a bar
instead of stranding the action at the far edge;
- a `role` override, for a bar that is page furniture rather than a
notification;
- `dense` now actually applies in `bar` mode (`py-1` vs `py-2`).

### Tests
`TabList.spec.ts` and `OTable.wrapMeasure.spec.ts` updated alongside.
2026-08-18 10:41:10 +00:00
ktx-vaidehiandgithub-actions[bot] c53a16b262 fix: script i18n (#13840)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-18 10:34:04 +00:00
71ac1071be fix: draft workflows (#13666)
Signed-off-by: Yashodhan Joshi <yjdoc2@gmail.com>
Co-authored-by: Yashodhan Joshi <yjdoc2@gmail.com>
Co-authored-by: Yashodhan Joshi <yashodhan@openobserve.ai>
2026-08-18 09:15:28 +00:00
Vamsi Karthik R 99b1a23773 test(slo): E2E + API suite for SLOs (#13836)
Adds automated test coverage for **SLOs** (#13547, #13577, #13579,
#13704, #13761, #13784), which shipped with none. **149 tests** — 77
Playwright + 72 pytest — all three SLI types, both query languages,
happy / negative / edge paths. Clean branch off `main`.

**UI (Playwright · `tests/ui-testing`)**
- `pages/sloPages/` — 4 page objects (verified `data-test` selectors,
hydration + Monaco-sync waits, OSelect commit-retry, virtualized-list
filtering).
- `playwright-tests/SLO/slo-crud.spec.js` (24) — create
count/time-slice/grouped through the form, detail, edit, SLI-type
change, tags, pause, delete, filters; negative: dismissed delete, empty
search, cancel, unmeasured SLI is absent not zero.
- `playwright-tests/SLO/slo-form-validation.spec.js` (20) — server
errors surfacing in the form, conditional fields per SLI type, count +
time-slice previews, **PromQL count and time-slice created end to end**;
negative: no stream, invalid aggregate, over-long name, target
boundaries.
- `playwright-tests/SLO/slo-alert-sli.spec.js` (8) — **the `alert` SLI
type**: source picker, create, config carries only the alert id, detail
names the source; negative: ineligible source offered-but-disabled,
grouped alert SLI, slice finer than source cadence.
- `playwright-tests/SLO/slo-timeslice.spec.js` (10) — **#13761 P0
regression** (measured SLI, not the preview), PromQL, previews, gaps,
language-flip clearing.
- `playwright-tests/SLO/slo-alerts.spec.js` (15) — burn-rate +
error-budget kinds, presets, edit, freeze door, burndown; negative: no
destination, cancel, blank/invalid name.
- `playwright-tests/utils/slo-seed.js` — backdated seeding, metrics
seeding, eligible-source-alert fixture, ground-truth query, fail-fast
env guard, transient-store retry, full fixture teardown (alerts →
streams → destinations → templates).

**API (pytest · `tests/api-testing`)**
- `tests/slos/test_slo_validation.py` (72) — full REST surface
(create/get/update/enable/delete/groups/slo-eligible), validation on
**both create and update** (name, target, window, slice, config shapes,
comparators, float-threshold round-trip), boundaries (target
0.001/99.999, name 256 vs 257, threshold 0/negative/1e12/NaN, unicode
names), alert-source eligibility (silence-gated, too-infrequent, cron),
PromQL config shapes, auth 401/404.

**CI**
- Two shards: `SLO` (`workers: 3`) and `SLO-Measurement` (`workers: 1`).
Split because the backfill job runs with
`ZO_SCHEDULER_SLO_BACKFILL_CONCURRENCY=1`, so measurement specs sharing
an instance starve each other's waits — `fullyParallel` races spec
*files*, which `mode:'serial'` cannot prevent. The 3-worker pin is
SQLite: at 5, concurrent SLO/alert/destination writes trip `database is
locked` on the single-node meta store.
- New optional per-shard `ingest_allowed_upto` (`240`) alongside the
existing `quick_mode_enabled`: the workflow default of 5 **hours**
silently drops the 8 days of history an SLO window needs, while still
answering `200`. Other shards keep the default. Same precedent as
`api-testing.yml`'s `48`.

**Feature code**: 9 additive `data-test`/`dataTest` attributes in
`SloDetail.vue`, `SloList.vue`, `SloAlertCondition.vue` — no logic
changes, `vue-tsc` clean. The stat tiles, tabs and burn-window inputs
had no stable selectors; `OStatStrip`/`OTab` already forward the
attribute.

**Behaviour**: self-cleaning (`e2e_slo_*`, worker-scoped since
`afterAll` runs per worker).

**Verified in CI** on this branch: `e2e / SLO` **52 passed** (1.8m,
workers pinned to 3), `e2e / SLO-Measurement` **25 passed** (11.3m,
workers pinned to 1) — the measurement shard passing confirms the
per-shard `ingest_allowed_upto` took effect, since those tests are
impossible under the 5-hour default. pytest **72 passed** locally; in
CI's 599-test run one setup delete hit a transient 500 (see findings)
and now retries.

**Findings for the feature owner** (reported, not fixed here):
- A blank "good when" shows `Request failed with status code 422` —
`pruned()` drops the empty key, so the API's real 400 message never
reaches `save()`.
- A rejected alert name shows no banner: `SloAlertForm.submit()` returns
on `nameError` before setting `saveError`.
- `avg(latency_ms); DROP TABLE users--` is accepted at create time
(parsed only at measurement).
- The name-length message says "less than 256 characters" but 256 is
accepted — 257 is the first rejection.
- Duplicate alert names on one SLO are accepted (duplicate SLO names are
a 409), and the panel lists them by name, so they are indistinguishable.
- SLO names accept characters that break the list's own
`slos-slolist-edit-${name}` selectors; alert names correctly reject
them.
- All chart/preview root `data-test` ids are unreachable — a parent's
fallthrough attribute overrides the child root's.
- Both edit forms hydrate asynchronously, so a blank form can flash
before the stored values appear.
- `DELETE /slos/{id}` on a freshly-created SLO returned **500** once
under CI load (not reproducible locally in 12 rapid iterations).
Contention is arguably a retryable 503, and the body carried no
actionable message.
- **A scheduled alert whose source stream is deleted is retried
forever** (`Stream "…" not found in schema`), and the SLO backfill lane
shares the scheduler's worker budget — so accumulated dead alerts
*starve SLO measurement*. Observed: 65 of them held coverage at 1 of 7
chunks indefinitely; deleting them restored 0.143 → 1.0 in 240s.
Harmless in CI (fresh instance per shard) but it degrades any long-lived
environment. The suite now cleans up after itself; the retry-forever
behaviour itself may deserve a look.

**Paired ENT PR: openobserve/o2-enterprise#2434.** ENT's matrix is this
repo's manifest plus an overlay, merged by this repo's
`build-ci-matrix.js`, so both SLO shards reach ENT automatically —
verified: 31 merged shards, both SLO entries carrying
`ingest_allowed_upto=240` and their worker pins, no other shard
affected. What ENT was missing is *consuming* those two fields, which
#2434 adds. Merge the two together: this one alone leaves ENT booting
the SLO shards on the 5-hour ingest window, and #2434 alone is inert.

*(An earlier review reported ENT as using an inline matrix with no SLO
shard — that turned out not to be the case.)*
2026-08-17 18:50:29 +00:00
69bd367d9b feat: notification dependency view — where-used for templates, destinations & alerts (#13820)
## What

Adds a **"View dependencies"** affordance to the **Templates**,
**Notification Destinations**, and **Alerts** lists. A small `graph-1`
icon on each row opens a compact popover that shows everything that
entity connects to along the **Template → Destination → Alert** chain,
with search, pagination, and per-row **Open** / **Delete**.

## Why

The linkage between alert templates, destinations, and alerts is
invisible today. There is **no reverse-lookup API** — the only way to
learn "which alerts use this destination" is to try deleting it and hit
the backend's `409 "Destination is currently used by alert: X"`. That's
a dead end, not a map.

This surfaces the dependency proactively so you can see the blast-radius
*before* you change or delete something, and act on it in place (open or
delete a related entity, with the same `409` guard still protecting
in-use items).

## Design

The three entities are linked **by name** (there are no ID foreign
keys):

- `Alert.destinations: string[]` → `Destination.name`
- `Destination.template` → `Template.name` (a destination's default)
- `Alert.template` → `Template.name` (an alert-level override)

```mermaid
flowchart LR
  T["Template"] -->|"Destination.template"| D["Destination"]
  D -->|"Alert.destinations&#91;&#93;"| A["Alert"]
  T -. "Alert.template (override)" .-> A
```

Because the alerts **list** DTO previously omitted `destinations`, the
whole graph would have needed N per-alert detail fetches. Instead a
small **opt-in additive field** on the list item makes the entire graph
**three list calls**, cross-referenced client-side and cached per-org:

```mermaid
flowchart TD
  Row["List row · graph-1 icon"] --> Pop["DependencyChainPopover<br/>(OPopover + ConfirmDialog)"]
  Pop --> Panel["DependencyUsagePanel<br/>search · collapsible sections · pager"]
  Panel --> Rows["DependencyUsageRow ×N<br/>Open · Delete"]
  Panel --> Load["useDependencyGraph.loadGraph()<br/>per-org cache"]
  Load -->|"alerts?include_dependencies=true<br/>+ destinations + templates (3 list calls)"| Build["buildGraph() → nodes + edges"]
  Panel --> Focus["buildFocusChain(graph, focus)<br/>directional: a shared template never<br/>drags in sibling destinations"]
  Rows -. "requestDelete(node)" .-> Pop
  Rows -. "open(node) — same-kind, in-place" .-> Pop
  Pop -. "ConfirmDialog → service.delete()<br/>(409 'used by' → toast)" .-> Done["emit deleted → list refreshes"]
```

Why the ConfirmDialog lives in the popover **wrapper**, not the panel: a
modal can't render inside the popover without dismissing it, so the
row/panel only *request* a delete and the wrapper owns the real
confirmation. **Open** works the same way for the in-place editors:
templates/destinations edit on their own list route (which only opens
from the page's mount-time handler), so a same-page open is bubbled to
the host page to invoke its native editor rather than a no-op
`router.push` to the same route.

## UX / behaviour

- **Where-used, grouped:** collapsible **Templates · Destinations ·
Alerts** sections, each a clickable row (kind-coloured icon, name, and
for destinations an "N alerts" **total-usage** badge).
- **Search-first:** one search box filters all groups — the key
affordance when a destination feeds thousands of alerts.
- **Paged:** the alerts group renders **50 at a time** with a pager
pinned beside its (sticky) header, so 4,000-alert destinations stay
responsive.
- **Act in place:** every row has **Open** (templates/destinations →
their editor; alerts → the alert's detail page) and **Delete** (the same
`ConfirmDialog` the list pages use; the backend `409` still blocks
in-use items).
- Consistent trigger on all three lists; **purely additive** — no
existing button, icon, or column was moved or replaced.

## Backend change

`ListAlertsResponseBodyItem`
(`src/api/management/src/models/alerts/responses.rs`) gains two additive
fields — the linkage the dependency view cross-references:

- `destinations: Vec<String>`
- `template: Option<String>` (the alert-level override)

They serialize **only when the caller passes
`?include_dependencies=true`** (which just the dependency popover does);
the default paginated alerts-list path is unchanged, so ordinary
consumers pay no extra bytes and receive no module-scoped
destination/template names. Both are also `skip_serializing_if`-empty.
Backward-compatible, OSS, no new endpoint.

## Testing

- **Unit (vitest) — 17 tests:**
- `useDependencyGraph.spec.ts` — `buildGraph` (linkage, unused/orphan,
dangling refs, override edges, shared destinations) and directional
`buildFocusChain` per focus kind (incl. the alert-focus total-usage
count).
- `DependencyUsagePanel.spec.ts` — sections render every kind, search +
50/page paging, collapse, `requestDelete`, the same-kind `open` bubble,
and the `include_dependencies` opt-in.
- `DependencyChainPopover.spec.ts` — trigger + lazy panel mount,
`requestDelete` → ConfirmDialog → API + `deleted`, and the `open` bubble
+ close.
- **Backend:** serialization test asserts `destinations`/`template` are
omitted when empty.
- `type-check`, `lint:design:strict`, and `prettier` all clean.
- A popover e2e (Playwright) is a follow-up in a separate PR — the
earlier spec targeted an interim dedicated page that this PR does not
ship, so it was removed.

## Notes

- Net change is popover-only: an earlier iteration added a dedicated
Reliability page + route + rail entry; those were dropped in favour of
the on-the-fly popover, so `router.ts` / `navGroups.ts` are untouched vs
`main`.
- Icon `graph-1` was added to the icon registry (additive) for the
popover trigger.
- **Deferred (see design doc Non-goals):** a server-side where-used /
`?destinations=` endpoint so the popover fetches only the neighbourhood
instead of the full alert list; and enterprise RBAC name-filtering on
the gated fields.

Design at:
https://github.com/openobserve/designs/blob/main/alerts/notification-dependencies/design.md
(designs#8: `alerts/notification-dependencies/design.md`)

---------

Co-authored-by: Abhay Padamani <141122205+ktx-abhay@users.noreply.github.com>
Co-authored-by: ktx-abhay <abhay.padamani@kiara.tech>
2026-08-17 12:00:39 +00:00
1c849a9c71 refactor(web): migrate templates + shadows to tokens, delete the debt baseline (#13769)
## What

Deletes `web/scripts/design-debt-baseline.json` — **not by relaxing the
rules, by fixing the debt**. Every design-token category is now
zero-tolerance, SCSS is gone from the app, and 144 hand-written shadow
values collapse into one scale.

## The guard was under-reporting (three separate holes)

1. **Comment contamination.** `check-design-consistency.mjs` fed comment
text into its exemption checks, so a `keep()` comment that merely
*named* `:deep(` / `::after` / `@keyframes` silenced the rule after it —
and a `;` or `{` inside a comment corrupted the offsets, so stripping
comments afterwards couldn't help. Comments are now blanked
(offset-preserving) before the scan. **Uncovered 9 occurrences in 6
files.** Real debt was 110, not 101.
2. **`arbShadow` only scanned the template.** `box-shadow:` in a
`<style>` block and `boxShadow:` in a JS/`:style` object were invisible
— 40 literals sat there while the guard reported zero. It now scans all
three syntaxes across `.vue` and `.ts`.
3. **`rawVarInTemplate` missed the fallback form.**
`border-(--color-x,rgba(…))` and `bg-[var(--color-x,#fff)]` slipped
through. The token is always defined, so the fallback can never fire —
it only hid the read.

## stylelint had never run on 14 files

`postcss-scss` isn't installed, so `postcss-html` **silently skipped**
every `<style lang="scss">` block. The CI step named *"Lint styles
(stylelint — bans legacy --o2-* tokens)"* was a no-op for those files —
including `color-no-hex` and the `.body--dark` selector ban. All 14 are
plain CSS now and `vue/block-lang` errors on `scss`/`sass`/`less`, so it
cannot silently recur.

## What moved

| | before | after |
|---|---|---|
| `rawVarInComponent` (in `<style>`) | 110 | **0** |
| template `x-[var(--color-*)]`, utility exists | 225 | **0** |
| template reads, no utility | 27 | **0** |
| `style` / `:style` colour literals | 29 | **0** |
| `color-mix(token N%, transparent)` | 160 | **0** |
| hand-written shadows | 233 (144 distinct) | **0** |
| SFCs using SCSS | 14 | **0** |

Guard categories: 22 → 24. `rawVarInTemplate` and `arbShadow` are new;
`arbPx` was deleted because main's `local/no-hardcoded-px` ESLint rule
(#13541) now owns px across every file type.

## Shadows are a system now

Stripped of colour, the 144 values were the same handful of geometries
rewritten in px vs rem, with and without an explicit `0` spread.

```
--shadow-xs / sm / md / lg              elevation
--shadow-sticky-left / -right           directional (sticky columns)
--shadow-rail                           inset status rail
--shadow-glow-sm / -md / -lg            coloured, 3 intensities
--shadow-scroll-top / -bottom / -both   scroll affordance
--shadow-ring-hairline                  1px ring where a utility can't reach
--shadow-ai-focus                       AI composer focus glow (flips in dark)
```

Colour is a **separate axis**. Tailwind rewrites a `--shadow-*` token as
`<geometry> var(--tw-shadow-color, <default>)` — verified by compiling a
probe, and it holds for multi-layer tokens — so a template writes
`shadow-md shadow-ai-accent/35`. A plain stylesheet can't add a utility,
so the role and glow tokens read `--glow-color`; that keeps the pipeline
canvas colour-coding nodes by type from CSS and lets the status rail be
painted per row from JS.

Also reclassified: `0 0 0 2px <colour>` is a **ring**, not a shadow, and
`0 1px 0 0 <colour>` is a **border**.

## color-mix, analysed by intent

All 331 occurrences were classified rather than treated as one problem:

- **160** mixed a token with `transparent` — only an alpha change, so
it's Tailwind's `/N` modifier written the long way (verified: `/N`
compiles to `color-mix(in oklab, <token> N%, transparent)`, and mixing
with `transparent` is premultiplied, so the interpolation space doesn't
matter)
- **81** blended two colours — a derived colour, so it's a token now
(~50 minted)
- **75** were shadow/ring geometry, handled by the scale
- **8 gradients + 1 `calc()`-driven percentage** genuinely have no
utility form and remain; the guard exempts gradient/mask/filter
contexts, matching what the style-block counter already exempts

## Merge with main

Main was running the same migration in parallel (**#13541 "px to rem"**
removed all SCSS, added `no-hardcoded-px`, and built its own shadow
scale), so the merge collided in 68 files / 155 hunks. Resolved by
taking main's source files and re-applying this branch on top, with the
guards driving convergence (429 → 0) rather than hand-resolving hunks.

On shadows, **main's structure wins** — geometry in `semantic.css`
`@theme`, colours as `--shadow-*-c*` sub-tokens, wrapped in its px
disable; its `--shadow-sm/md/lg`, `--shadow-scroll-*` and
`--shadow-glow` keep their definitions. This branch adds what main
didn't have: `xs`, the directional/inset roles, and the
elevation-matched coloured glows.

## New tokens

No new *colours* — each names a value a component already hardcoded,
because a registered palette utility (`text-orange-500`) is banned by
`rawPalette`, so "just register the ramp" was never available.

`--color-splitter-hover`, `--color-status-flaky` (+`-text`),
`--color-chat-bubble-user-border`, `--color-thread-avatar-bg`,
`--color-thread-accent-strong`, `--color-menu-badge-ring`,
`--color-progress-track-fill`, `--color-ingest-accent-*` (5), ~50
derived blend tokens, plus `--color-border-strong` /
`--color-border-subtle` / `--color-warning-300` registered for utility
use.

## Visual deltas to review

Everything else is value-preserving. These are not:

- **4 hover accents** move from a fixed `primary-600` to `accent` — 600
→ 400 **in dark only** (2 chevrons, an empty-state icon fill, one link)
- **sticky table header** in `TraceDetailsSidebar` was a fixed light
grey that never flipped; now `border-default`, so it themes
- **shadow canonicalization** snaps near-identical geometries together
and moves ~40 hardcoded `rgba(0,0,0,x)` onto the scale's token colour

Highest-surface single file: `AlertSettingsHelpDrawer.vue` — a whole
style block deleted with ~44 class names moved into template utilities.

## Verification

`lint:design` · `lint:tokens` · `lint:token-purity` · `lint:styles` ·
`format:check` · `lint:ci` · `vue-tsc` — all clean.
**94 spec files covering every file this branch touches: 4395 passing, 0
failing.**

Not run: the full 1236-file unit suite, Playwright/E2E, and any browser
visual check.

---------

Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech>
Co-authored-by: Abhay Padamani <141122205+ktx-abhay@users.noreply.github.com>
Co-authored-by: ktx-abhay <abhay.padamani@kiara.tech>
Co-authored-by: ktx-vaidehi <134508096+ktx-vaidehi@users.noreply.github.com>
Co-authored-by: Bhargav <BJP232004@GMAIL.COM>
2026-08-17 11:46:07 +00:00
Abhay Padamaniandgithub-actions[bot] f112ac01a5 fix: dashboard layout improvements (#13815)
<h2 style="unicode-bidi: plaintext; color: rgb(204, 204, 204);
font-family: -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;,
Roboto, sans-serif; font-style: normal; font-variant-ligatures: normal;
font-variant-caps: normal; letter-spacing: normal; orphans: 2;
text-align: start; text-indent: 0px; text-transform: none; widows: 2;
word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal;
background-color: rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color:
initial;">Dashboard layout &amp; UX improvements</h2><p
style="white-space: pre-wrap; margin-top: 0.1em; margin-bottom: 0.2em;
unicode-bidi: plaintext; color: rgb(204, 204, 204); font-family:
-apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, Roboto,
sans-serif; font-size: 13px; font-style: normal; font-variant-ligatures:
normal; font-variant-caps: normal; font-weight: 400; letter-spacing:
normal; orphans: 2; text-align: start; text-indent: 0px; text-transform:
none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;
background-color: rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color: initial;">Polish
pass on the dashboard experience: settings layout, drag/resize grid
feedback, empty states, and per-tab panel counts.</p><h3
style="unicode-bidi: plaintext; color: rgb(204, 204, 204); font-family:
-apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, Roboto,
sans-serif; font-style: normal; font-variant-ligatures: normal;
font-variant-caps: normal; letter-spacing: normal; orphans: 2;
text-align: start; text-indent: 0px; text-transform: none; widows: 2;
word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal;
background-color: rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color: initial;">✨
Highlights</h3><p style="white-space: pre-wrap; margin-top: 0.1em;
margin-bottom: 0.2em; unicode-bidi: plaintext; color: rgb(204, 204,
204); font-family: -apple-system, &quot;system-ui&quot;, &quot;Segoe
UI&quot;, Roboto, sans-serif; font-size: 13px; font-style: normal;
font-variant-ligatures: normal; font-variant-caps: normal; font-weight:
400; letter-spacing: normal; orphans: 2; text-align: start; text-indent:
0px; text-transform: none; widows: 2; word-spacing: 0px;
-webkit-text-stroke-width: 0px; background-color: rgb(24, 24, 24);
text-decoration-thickness: initial; text-decoration-style: initial;
text-decoration-color: initial;"><strong>Grid feedback while
dragging/resizing panels</strong> (<a
href="vscode-webview://0vkpvjgb486eanj04qjfqcnqmnatevjacbbbqdp3ecrs4efo5krm/web/src/views/Dashboards/RenderDashboardCharts.vue"
target="_blank" rel="noopener noreferrer" style="color: rgb(77, 170,
252); text-decoration: rgb(77, 170,
252);">RenderDashboardCharts.vue</a>)</p><ul
style="padding-inline-start: 2em; color: rgb(204, 204, 204);
font-family: -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;,
Roboto, sans-serif; font-size: 13px; font-style: normal;
font-variant-ligatures: normal; font-variant-caps: normal; font-weight:
400; letter-spacing: normal; orphans: 2; text-align: start; text-indent:
0px; text-transform: none; widows: 2; word-spacing: 0px;
-webkit-text-stroke-width: 0px; white-space: normal; background-color:
rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color: initial;"><li
style="unicode-bidi: plaintext;">Added a live alignment grid that
appears only during a drag or resize, drawn from GridStack's real cell
metrics (192 columns × row height).</li><li style="unicode-bidi:
plaintext;">The grid is<span> </span><strong>operation-aware</strong>:
lines snap to the top/left edges
while<span> </span><em>moving</em><span> </span>and the bottom/right
edges while<span> </span><em>resizing</em>, so the edge you're dragging
always lands on a line and every line is reachable (no skipped
positions).</li><li style="unicode-bidi: plaintext;">Panels are made
opaque during interaction (<code style="font-family: monospace; color:
rgb(208, 208, 208); background-color: rgb(60, 60, 60); padding: 2px 4px;
border-radius: 3px; word-break: break-word; font-size:
0.9em;">--color-surface-base</code>) so the grid only shows in the
gutters/empty cells, not through chart or table panels.</li></ul><p
style="white-space: pre-wrap; margin-top: 0.1em; margin-bottom: 0.2em;
unicode-bidi: plaintext; color: rgb(204, 204, 204); font-family:
-apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, Roboto,
sans-serif; font-size: 13px; font-style: normal; font-variant-ligatures:
normal; font-variant-caps: normal; font-weight: 400; letter-spacing:
normal; orphans: 2; text-align: start; text-indent: 0px; text-transform:
none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;
background-color: rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color:
initial;"><strong>Empty states</strong> (<a
href="vscode-webview://0vkpvjgb486eanj04qjfqcnqmnatevjacbbbqdp3ecrs4efo5krm/web/src/lib/core/EmptyState/presets.ts"
target="_blank" rel="noopener noreferrer" style="color: rgb(77, 170,
252); text-decoration: rgb(77, 170, 252);">presets.ts</a>, <a
href="vscode-webview://0vkpvjgb486eanj04qjfqcnqmnatevjacbbbqdp3ecrs4efo5krm/web/src/components/dashboards/settings/VariableSettings.vue"
target="_blank" rel="noopener noreferrer" style="color: rgb(77, 170,
252); text-decoration: rgb(77, 170, 252);">VariableSettings.vue</a>, <a
href="vscode-webview://0vkpvjgb486eanj04qjfqcnqmnatevjacbbbqdp3ecrs4efo5krm/web/src/views/Dashboards/ScheduledDashboards.vue"
target="_blank" rel="noopener noreferrer" style="color: rgb(77, 170,
252); text-decoration: rgb(77, 170,
252);">ScheduledDashboards.vue</a>)</p><ul style="padding-inline-start:
2em; color: rgb(204, 204, 204); font-family: -apple-system,
&quot;system-ui&quot;, &quot;Segoe UI&quot;, Roboto, sans-serif;
font-size: 13px; font-style: normal; font-variant-ligatures: normal;
font-variant-caps: normal; font-weight: 400; letter-spacing: normal;
orphans: 2; text-align: start; text-indent: 0px; text-transform: none;
widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;
white-space: normal; background-color: rgb(24, 24, 24);
text-decoration-thickness: initial; text-decoration-style: initial;
text-decoration-color: initial;"><li style="unicode-bidi:
plaintext;">Replaced the generic<span> </span><code style="font-family:
monospace; color: rgb(208, 208, 208); background-color: rgb(60, 60, 60);
padding: 2px 4px; border-radius: 3px; word-break: break-word; font-size:
0.9em;">NoData</code><span> </span>placeholder with<span> </span><code
style="font-family: monospace; color: rgb(208, 208, 208);
background-color: rgb(60, 60, 60); padding: 2px 4px; border-radius: 3px;
word-break: break-word; font-size:
0.9em;">OEmptyState</code><span> </span>for the Variables settings table
and the Scheduled Dashboards list.</li><li style="unicode-bidi:
plaintext;">Added a new<span> </span><code style="font-family:
monospace; color: rgb(208, 208, 208); background-color: rgb(60, 60, 60);
padding: 2px 4px; border-radius: 3px; word-break: break-word; font-size:
0.9em;">no-variables</code><span> </span>preset (illustration + copy +
"Add variable" action); reused the existing<span> </span><code
style="font-family: monospace; color: rgb(208, 208, 208);
background-color: rgb(60, 60, 60); padding: 2px 4px; border-radius: 3px;
word-break: break-word; font-size:
0.9em;">no-reports</code><span> </span>preset for scheduled
dashboards.</li><li style="unicode-bidi: plaintext;">Fixed the Variables
table so a long
list<span> </span><strong>scrolls</strong><span> </span>instead of
clipping, and made the Scheduled Dashboards table fill the drawer height
so the empty state centers.</li></ul><p style="white-space: pre-wrap;
margin-top: 0.1em; margin-bottom: 0.2em; unicode-bidi: plaintext; color:
rgb(204, 204, 204); font-family: -apple-system, &quot;system-ui&quot;,
&quot;Segoe UI&quot;, Roboto, sans-serif; font-size: 13px; font-style:
normal; font-variant-ligatures: normal; font-variant-caps: normal;
font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start;
text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px;
-webkit-text-stroke-width: 0px; background-color: rgb(24, 24, 24);
text-decoration-thickness: initial; text-decoration-style: initial;
text-decoration-color: initial;"><strong>Dashboard settings
layout</strong> (<a
href="vscode-webview://0vkpvjgb486eanj04qjfqcnqmnatevjacbbbqdp3ecrs4efo5krm/web/src/components/dashboards/settings/GeneralSettings.vue"
target="_blank" rel="noopener noreferrer" style="color: rgb(77, 170,
252); text-decoration: rgb(77, 170,
252);">GeneralSettings.vue</a>)</p><ul style="padding-inline-start: 2em;
color: rgb(204, 204, 204); font-family: -apple-system,
&quot;system-ui&quot;, &quot;Segoe UI&quot;, Roboto, sans-serif;
font-size: 13px; font-style: normal; font-variant-ligatures: normal;
font-variant-caps: normal; font-weight: 400; letter-spacing: normal;
orphans: 2; text-align: start; text-indent: 0px; text-transform: none;
widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;
white-space: normal; background-color: rgb(24, 24, 24);
text-decoration-thickness: initial; text-decoration-style: initial;
text-decoration-color: initial;"><li style="unicode-bidi:
plaintext;">Reworked the General settings layout.</li></ul><p
style="white-space: pre-wrap; margin-top: 0.1em; margin-bottom: 0.2em;
unicode-bidi: plaintext; color: rgb(204, 204, 204); font-family:
-apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, Roboto,
sans-serif; font-size: 13px; font-style: normal; font-variant-ligatures:
normal; font-variant-caps: normal; font-weight: 400; letter-spacing:
normal; orphans: 2; text-align: start; text-indent: 0px; text-transform:
none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;
background-color: rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color:
initial;"><strong>Tab panel-count badge</strong> (<a
href="vscode-webview://0vkpvjgb486eanj04qjfqcnqmnatevjacbbbqdp3ecrs4efo5krm/web/src/components/dashboards/tabs/TabList.vue"
target="_blank" rel="noopener noreferrer" style="color: rgb(77, 170,
252); text-decoration: rgb(77, 170, 252);">TabList.vue</a>)</p><ul
style="padding-inline-start: 2em; color: rgb(204, 204, 204);
font-family: -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;,
Roboto, sans-serif; font-size: 13px; font-style: normal;
font-variant-ligatures: normal; font-variant-caps: normal; font-weight:
400; letter-spacing: normal; orphans: 2; text-align: start; text-indent:
0px; text-transform: none; widows: 2; word-spacing: 0px;
-webkit-text-stroke-width: 0px; white-space: normal; background-color:
rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color: initial;"><li
style="unicode-bidi: plaintext;">Each dashboard tab now shows a badge
with its panel count (section headers excluded), so tab density is
visible without opening each tab.</li></ul><p style="white-space:
pre-wrap; margin-top: 0.1em; margin-bottom: 0.2em; unicode-bidi:
plaintext; color: rgb(204, 204, 204); font-family: -apple-system,
&quot;system-ui&quot;, &quot;Segoe UI&quot;, Roboto, sans-serif;
font-size: 13px; font-style: normal; font-variant-ligatures: normal;
font-variant-caps: normal; font-weight: 400; letter-spacing: normal;
orphans: 2; text-align: start; text-indent: 0px; text-transform: none;
widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;
background-color: rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color:
initial;"><strong>Variable dependency indicator</strong> (<a
href="vscode-webview://0vkpvjgb486eanj04qjfqcnqmnatevjacbbbqdp3ecrs4efo5krm/web/src/components/dashboards/settings/VariableSettings.vue"
target="_blank" rel="noopener noreferrer" style="color: rgb(77, 170,
252); text-decoration: rgb(77, 170,
252);">VariableSettings.vue</a>)</p><ul style="padding-inline-start:
2em; color: rgb(204, 204, 204); font-family: -apple-system,
&quot;system-ui&quot;, &quot;Segoe UI&quot;, Roboto, sans-serif;
font-size: 13px; font-style: normal; font-variant-ligatures: normal;
font-variant-caps: normal; font-weight: 400; letter-spacing: normal;
orphans: 2; text-align: start; text-indent: 0px; text-transform: none;
widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;
white-space: normal; background-color: rgb(24, 24, 24);
text-decoration-thickness: initial; text-decoration-style: initial;
text-decoration-color: initial;"><li style="unicode-bidi:
plaintext;">Variables that depend on others show a "Depends on: …" chip
(tree icon + count) in the settings table.</li></ul><p
style="white-space: pre-wrap; margin-top: 0.1em; margin-bottom: 0.2em;
unicode-bidi: plaintext; color: rgb(204, 204, 204); font-family:
-apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, Roboto,
sans-serif; font-size: 13px; font-style: normal; font-variant-ligatures:
normal; font-variant-caps: normal; font-weight: 400; letter-spacing:
normal; orphans: 2; text-align: start; text-indent: 0px; text-transform:
none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;
background-color: rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color:
initial;"><strong>Panel header token</strong> (<a
href="vscode-webview://0vkpvjgb486eanj04qjfqcnqmnatevjacbbbqdp3ecrs4efo5krm/web/src/lib/styles/tokens/component.css"
target="_blank" rel="noopener noreferrer" style="color: rgb(77, 170,
252); text-decoration: rgb(77, 170, 252);">component.css</a>)</p><ul
style="padding-inline-start: 2em; color: rgb(204, 204, 204);
font-family: -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;,
Roboto, sans-serif; font-size: 13px; font-style: normal;
font-variant-ligatures: normal; font-variant-caps: normal; font-weight:
400; letter-spacing: normal; orphans: 2; text-align: start; text-indent:
0px; text-transform: none; widows: 2; word-spacing: 0px;
-webkit-text-stroke-width: 0px; white-space: normal; background-color:
rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color: initial;"><li
style="unicode-bidi: plaintext;">Lightened<span> </span><code
style="font-family: monospace; color: rgb(208, 208, 208);
background-color: rgb(60, 60, 60); padding: 2px 4px; border-radius: 3px;
word-break: break-word; font-size:
0.9em;">--color-panel-bar-bg</code><span> </span>(grey-200 → grey-100)
for a softer chart-header band; the grey-300 divider still carries the
header/body edge.</li></ul><h3 style="unicode-bidi: plaintext; color:
rgb(204, 204, 204); font-family: -apple-system, &quot;system-ui&quot;,
&quot;Segoe UI&quot;, Roboto, sans-serif; font-style: normal;
font-variant-ligatures: normal; font-variant-caps: normal;
letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px;
text-transform: none; widows: 2; word-spacing: 0px;
-webkit-text-stroke-width: 0px; white-space: normal; background-color:
rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color: initial;">

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-17 10:02:32 +00:00
udev d770d0245a fix: preserve online evaluation provider API keys (#13739)
## Summary

- preserve the stored provider API key when an edit submits a blank
value
- replace the stored key when a nonblank value is supplied
- show the create-time required marker only for providers that require
authentication
- retain keyless creation for self-hosted providers

## Testing

- ProviderFormPage Vitest suite passed: 8 tests
- Targeted ESLint passed
- cargo fmt check passed
- git diff check passed
- Focused Rust test compilation is blocked by unrelated existing
alert-state model errors

Related to #13655.

Companion enterprise PR:
https://github.com/openobserve/o2-enterprise/pull/2390
2026-08-17 05:08:15 +00:00
Bhargav b9e11bb885 fix(web): classify unquoted LIKE patterns wherever the parser stops (#13818)
`x LIKE E00%` parses as the identifier `E00` followed by the modulo
operator, so the PEG parser fails on a later token and the existing
`found === "%"` check never fires — the error fell through to the
allow-list suppression and the editor showed no diagnostic at all. Only
patterns whose first character terminates the identifier (`LIKE BIN-%`)
were caught.

Add hasUnquotedLikePattern(), which inspects the LIKE/ILIKE/RLIKE
operand nearest the error position and reports badLikePattern when it is
a bare wildcard token. It runs immediately before the suppression
return, so it can only reclassify errors that were previously dropped;
quoted patterns, parenthesised expressions and function calls never
match.

Detection on the unquote_like_pattern mutation set goes 6/13 -> 13/13,
so raise that threshold from 0.5 to 0.9. The old 0.5 sat exactly on the
sample boundary, which is why adding one query to the shared query-agent
corpus in #13808 turned the suite red.

Also stop the per-mutation tests from regenerating the whole mutation
pool (~1,000 queries x 16 mutations, each re-parsed) on every case:
memoize generateBrokenQueries() and give the heavy tests an explicit 30s
timeout. Spec runtime drops from ~112s to ~48s and the 5s-default
timeouts go away.
2026-08-17 04:48:23 +00:00
Omkar Kesarkhaneandgithub-actions[bot] ddb67f3efb fix(synthetics): record from a specific step, and stop silently downgrading recorded actions (#13799)
Supersedes #13777, which carried the record-from-step commits alone.
Closing that in favour of this, because the two changes land in the same
three files (`types/synthetics.ts`, `useSyntheticsRecorder.ts`,
`JourneySteps.vue`) and reviewing them apart means resolving the same
conflict twice.

## Problem

**A recorded hover was stored as a click.**

Playwright 1.56 added `hover` to the recorder's action model, reachable
from its "Choose action" picker. The extension's action mapper seeds
every step with `action: 'click'` as a placeholder for its switch to
overwrite, and returned that placeholder from `default:` — so any action
name the switch did not know became a click the author never performed.

On the usual hover target — a nav trigger, a dropdown opener — clicking
it can navigate or submit. The run still reports green.

There are three separate guards meant to stop exactly this, and none of
them fired, because the relabel happens upstream of all of them:

| Guard | Why it missed |
|---|---|
| `UNSUPPORTED_REPLAY_ACTIONS` (extension) | step already said `click` |
| `RETIRED_ACTIONS` → `isStorableAction` | step already said `click` |
| `V2_STEP_ACTIONS` (Rust validation) | `click` is valid |

The same picker offers **Right click** and **Double click** as explicit
choices. Both were flattened too: `click_count` was dropped by the
mapper and never existed downstream, and the probe's executor called
`locator.click({ timeout })` while reading neither `button` nor
`click_count`. A deliberately recorded right click replayed as a left
click; a double click as a single one.

**Recording could only start from scratch.** Fixing step 12 of a 15-step
journey meant re-recording all 15.

## Change

**Record from a specific step.** Each step gets a record action that
replays the prefix before it, then flips the recorder into insert mode
at that point. A restore that cannot reach the requested step reports
where it stopped (`prefixFailed`) rather than silently recording from
the wrong place.

**Every action Playwright can record is now stored and replayed as
itself.**

- `hover` is a first-class v2 action — mapped, stored, validated, and
executed by `Locator.hover` in the probe and `Frame.hover` in the
extension player.
- `button` and `click_count` travel to storage, so right click and
double click replay as recorded.
- An action name the mapper does not know now **throws** instead of
returning the `click` placeholder. This is the part that is not
hover-specific: upstream added one action in nine minors and will add
more, and the next one must be a deliberate decision rather than a
silent downgrade.

**The extension handshake reports capabilities, not just presence.**
`getStatus` now returns `extVersion` and a `capabilities` array; O2
gates affordances on `hasCapability()` with a defined pre-handshake
fallback, and warns when the installed build is older than the minimum.
Previously the protocol carried no version at all — Chrome updates the
extension independently of when O2 deploys, so the two could disagree
about the wire with no way to say so.

**Two dead message declarations, opposite problems.** `setSources` no
longer exists in playwright-core (sources arrive alongside the actions),
so its declared variant could never fire — while its `generatedCode`
field is exactly what an eject-to-code feature would reach for. Deleted.
`elementPicked` is the reverse: it fires on every "Pick locator" and
every click in inspecting mode, and was being discarded, which made
inspect mode a dead end. Now kept.

### Compatibility

`button` and `click_count` are optional with defined absent-behaviour —
absent means a single left click. Every journey stored today stays
valid, means the same thing, and serialises byte-identically. **No
schema version bump and no migration.** A test pins that rather than
leaving it as an intention.

`hover` is a new vocabulary entry with no default, so the probe must be
able to execute one before O2 is allowed to store one — the probe change
ships first.

## Implementation

| File | |
|---|---|
| `src/config/src/meta/synthetics.rs` | `hover` joins
`V2_STEP_ACTIONS`/`V2_ELEMENT_ACTIONS`; `button` and `click_count` added
to `BrowserStepV2` with validation (`left\|middle\|right`, `1..=3`) |
| `web/src/types/synthetics.ts` | `button`/`clickCount` on both step
types; `extVersion` + `capabilities` on `RecorderStatus`; `setSources`
variant deleted and `elementPicked` typed; `prefixFailed` and
insert-mode fields on `recordingStarted` |
| `web/src/utils/synthetics/mapRecordedStep.ts` | promotes
`button`/`clickCount` from the wire, like every other stored field |
| `web/src/utils/synthetics/buildV2Steps.ts` | `hover` in
`ACTION_TO_V2`; crosses camelCase → snake_case for the two click fields
|
| `web/src/constants/synthetics.ts` | `hover` out of `RETIRED_ACTIONS`;
`MIN_EXTENSION_VERSION` |
| `web/src/composables/useSyntheticsRecorder.ts` | `startRecordingFrom`,
`hasCapability`, `pickedSelector`, `extensionOutdated` |
|
`web/src/components/synthetics/journey/{BrowserJourney,JourneySteps}.vue`
| per-step record action and its wiring |

`click_count` is capped at 3: the recorder only ever emits 1 or 2, and
an unbounded count is a way to hold a page's event loop open.

The two click fields cross from camelCase to snake_case in `buildV2Step`
and nowhere else — reading `step.wire` directly inside it would have
worked and invented a second path into storage.

### Depends on a recorder extension build

`hover`, the click fields and the capability handshake all need the
matching extension build. O2 degrades honestly without it:
`hasCapability()` falls back to the pre-handshake set, and an older
build raises the outdated-extension warning instead of failing on a
message shape it does not recognise.

## Tests

- **Rust** — hover accepted as an element action; `button`/`click_count`
validated at both bounds; a step carrying neither still validates (the
compatibility guarantee). Two existing tests encoded "hover is rejected"
and now say the opposite. 101 pass.
- **Probe** — hover hovers rather than clicks; a right click sends the
recorded button; a double click sends two clicks; a journey stored
before these fields replays unchanged. The action union is asserted
against the Rust vocabulary, so the next divergence fails a test instead
of reaching an executor with no case for it. Also pinned: a selector
carrying a frame hop resolves into the iframe, which had no coverage
anywhere.
- **Web** — 1213 across 60 files. Nine existing tests encoded "hover is
retired" across constants, `buildV2Steps`, `buildPayload` and the
save-gate schema; each now states the opposite deliberately.
- **Extension** — the unmapped-action throw, hover round-tripping both
directions, and a double click surviving the round trip as two clicks.

`vue-tsc`, `cargo clippy -D warnings`, and `prettier --check` are clean.
The last commit is formatting only: the three record-from-step files
arrived failing `format:check`, which is a gate of its own and would not
have been caught by lint.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-16 17:50:46 +00:00
Hengfei Yang 192f11f7ea fix(i18n): correct mistranslations in zh-CN locale (#13835)
## Summary

Fixes machine-translation errors in the Simplified Chinese locale
(`web/src/locales/languages/zh-CN.json`), found by sweeping the file
against the English source. Three batches of the same class of error:

### Nonsense translations (wrong sense of the English word)
| Before | After | English source |
|---|---|---|
| 痕迹 / 踪迹 (physical traces) | 追踪 | Traces (nav, tabs, buttons — 8 keys)
|
| 朗姆酒 (the liquor) | RUM | RUM |
| 漂亮 / 崩溃了 (beautiful / crashed) | 格式化 / 折叠 | Pretty / Collapsed view
toggle |
| 汽车 (a car) | 自动 | Auto (layout mode) |
| 转 / 转弯 (road turns) | 回合 | Turn / Turns (conversation) |
| {n} 个电话 (phone calls) | {n} 次调用 | {n} calls (tool calls) |
| oTel 收藏家/代理人 (art collector) | OTel Collector / 代理 | OTel Collector /
Agent |
| 空缺评测任务 (job vacancy) | 打开评估任务 | Open Eval Jobs |
| 简历 (a CV) | 恢复 | Resume (pause/resume) |
| 出口 (trade export, ×3) | 导出 | Export |
| 逃脱了 (ran away) | 已转义 | Escaped (string escaping) |
| 测试赛 (sports match) | 测试匹配 | Test Match |
| 比分 (sports score) | 分数 | scores (eval scores) |
| 好吧 ("well, fine") | 正常 | Ok (status) |
| 领域 (domain/field) | 区域 | Region (bucket region) |
| 全球 (worldwide, ×4) | 全局 | Global (scope) |
| 商店 (a shop) | 存储 | Store (key store) |
| 尺寸 (physical size, ×5) | 维度 | Dimensions (metrics) |
| 用法 / 使用密钥 (how-to / secret key) | 用量 / 用量键 | Usage / Usage key |
| 滤镜 (photo filters) | 筛选条件 | Filters |
| 网络缓存影响 (network) | 缓存净影响 | Net Cache Impact |
| 申请并搜索 (apply for) | 应用并搜索 | Apply & Search |
| 压缩后的尺寸 | 压缩后大小 | Compressed Size |
| 等得紧,我们正在搜寻你的踪迹。 | 正在加载追踪... | Loading Traces... |

### Terminology unification
- **GenAI agents → 智能体** (agent graph, agent behavior, agent mapping,
sessions — ~40 strings). Infrastructure agents (host collectors,
synthetics runners) keep the conventional 代理.
- **Eval jobs → 评估任务** (was a mix of 评估作业 / 评测任务 / 作业). Batch jobs
elsewhere (backfill, search jobs) keep 作业.
- **Enrichment table → 富化表** (was a mix of 充实表 / 丰富表 / 扩充表).
- **Cipher Key → 加密密钥** (was mixed with 密码密钥); **Actions → 操作** (was 行动
in 4 places); **Running Queries → 运行中的查询**; synthetics **journey → 流程**
(was 旅程); assistant 助理 → 助手.
- `shortcuts.actions.tracesSearch` resynced with current English source
("Run query" → 运行查询).

### Cosmetic
- Shortcuts cheatsheet: corrupted ⌘ symbol (rendered as ▼) restored;
ungrammatical strings fixed (按?任何地方 → 在任意位置按 ?, 焦点搜索栏 → 聚焦搜索栏, 导入控制面板 →
导入仪表板); untranslated example values restored to `input`/`output`;
half-translated strings completed (Spans 表格视图 → 跨度表格视图, Stacktrace 已复制 →
堆栈跟踪已复制).

Notes:
- zh-TW.json was checked for the same error patterns and is clean.
- 跟踪 vs 追踪 for "trace" is still mixed in a few longer sentences; left
as-is since 跟踪 is not incorrect, only inconsistent.

## Test plan
- JSON validated with `python3 -c 'json.load(...)'` after each batch;
keys unchanged, only values edited (~200 lines).
- Grep confirms none of the wrong terms above remain in zh-CN.json.
2026-08-16 15:52:36 +00:00
Harsh Mahajan 7f74925605 feat: banner as notification (#13667) 2026-08-16 13:42:35 +00:00
Hengfei Yang c246119809 fix(i18n): stop translated placeholder names crashing localized pages (#13833)
Fixes #13751

## The crash

vue-i18n compiles messages just-in-time (`__INTLIFY_JIT_COMPILATION__`),
and a compile error is **thrown**, not warned — it tears down whatever
component asked for the message. The translation pipeline had been
localizing placeholder *names*, which is exactly that kind of error:

```
en-US  "Are you sure you want to delete {identifier}?"
zh-CN  "您确定要删除 {标识符} 吗?"        -> SyntaxError: 2 (INVALID_TOKEN_IN_PLACEHOLDER)
```

IAM → Service Accounts rendered blank in Chinese for this reason. The
two zh-CN strings named in the issue had already been patched by hand in
#13707, but the *class* was never swept.

## What was actually broken

Compiling every message in every locale file with vue-i18n's own
compiler found three groups:

| | count | symptom |
|---|---|---|
| Does not compile | 132 | page blanks with `SyntaxError` — 8 locales:
es, fr, it, ja, ko, nl, pt, tr |
| Compiles, unknown name | 147 | `{Anzahl}` for `{count}` → value
renders as an empty string |
| Stale text, dropped placeholder | 5 | `{org}`, `{payer}`,
`{endpoint}`, `{service}` — English source no longer has them |

Worst of the first group are mangled vue-i18n literal escapes, e.g.
`{'{'}team{'}'}` coming back as `{'{'} '}` — an unbalanced brace.

All 279 are corrected here. The existing translated prose is kept
verbatim; only the tokens are restored. The 5 stale ones are
retranslated from the current English text, since restoring a token that
no longer exists is not an option.

## Why it can't come back

**`web/src/locales/enUsMessages.spec.ts` already did this — for en-US
only.** That left the machine-translated locales, the actual source of
the bug, uncovered. Renamed to `localeMessages.spec.ts` and extended to
every locale, plus a second assertion that a translation only
interpolates names en-US defines.

That second check is a **subset** rule, not equality: dropping a
placeholder the target language does not need stays legal (an English
plural suffix like `{plural}` has no Japanese equivalent). Introducing a
name en-US never passes a value for is always a bug.

Placeholder names are collected by *running* the compiled message
against a recording context rather than by pattern-matching the source,
because only the compiler knows which braces are placeholders and which
are literal escapes — `{'}'}` is one token whose body is the character a
`{...}` regex stops at. All plural branches are evaluated.

**`scripts/translations/translator.py` — `_validate` had the same regex
blind spot** (`{[^{}]*}`) and never checked that the output compiles at
all, which is why these landed. It now tokenizes the way vue-i18n does
and rejects anything vue-i18n would refuse, so the pipeline stops
emitting them rather than CI merely catching them afterwards. Escape
drift (`{'{'} rows {'} rows {'}`) is caught too, because literal escapes
are now part of the compared token multiset.

## Verification

- The Python scanner was cross-checked against vue-i18n's compiler over
**all 170,055 messages** in the 15 locale files — identical compile
verdicts and identical token sets, 0 divergences.
- Injecting the original `{标识符}` string back into zh-CN fails the new
spec; injecting a compiling-but-wrong `{bilv}` fails the second
assertion.
- `npx vitest run --environment jsdom --root src/ locales/` → 45 passed.
`prettier --check` and `tsc` clean on the spec.
- Only `en-US.json` is imported anywhere in `web/src`, so no existing
test depends on the changed locale data.

## Not in scope

A handful of translations are awkward but compile and bind correctly
(e.g. ko-KR renders "ingestion limit" with the eating sense of the
word). Left alone to keep this diff auditable as a mechanical token
restoration.
2026-08-16 09:27:40 +00:00
Prabhat Sharmaandgithub-actions[bot] 0dba67d638 feat: composite alerts (#13797)
Design at : alerts/composite_alerts/composite_alerts.md

## Summary
- add composite alert graph persistence, evaluation, scheduling,
lifecycle APIs, and deletion safeguards
- add composite alert UI forms, preview, references, list/detail
integration, and localization
- add Rust, API, Vue, and Playwright contracts

## Verification
- cargo fmt --all -- --check
- cargo check -p openobserve-core
- cargo check -p openobserve-api-management
- npm run type-check:composite-alerts
- npm run type-check:app
- npm run lint:ci -- --quiet
- design/token/style checks
- targeted Rust/Vitest suites
- browser QA against local alerts UI

## Follow-ups (intentionally deferred, documented in the spec)
- `RunOutcome::Error`/`NotifyFailed` outcome records on composite
delivery failure (§11.5)
- Incident detail live-definition resolver for composites + "definition
unavailable" retention (§9.6)
- `alert_composite_*` observability metrics (§16)
- SLO/stream delete-conflict 409 bodies with readable-vs-hidden parent
split (§10.3)
- Super-cluster startup preflight (§18)

<img width="1650" height="979" alt="image"
src="https://github.com/user-attachments/assets/3def5246-7bf7-48fe-9a42-c0ac08271ea9"
/>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-15 00:57:20 +00:00
Hengfei Yang 3fda083f51 feat(traces): add trace time index (#13827)
Design at:
[https://github.com/openobserve/designs/blob/main/infrastructure/file-list/trace-time-index.md](https://github.com/openobserve/designs/blob/main/infrastructure/file-list/trace-time-index.md)

## Summary

- add an hourly Metadata time index per trace stream to resolve a trace
ID to its full time range
- add external time-range and trace-details APIs, including bounded
locate/expand behavior and standard search responses
- integrate index generation into trace ingestion and preserve query
visibility through the synchronous Metadata write path
- synchronize retention and deletion for associated Metadata streams
- reuse the shared trace stream permission check across trace APIs

## Why

Trace detail lookup currently depends on a caller-provided time range,
which can omit spans outside that window. The time index provides a
bounded, best-effort range lookup for `stream + trace_id` without
scanning the full trace stream.

Closes #13823

## Environment variables

| Variable | Default | Effect |
| --- | --- | --- |
| `ZO_TRACE_TIME_INDEX_ENABLED` | `true` | Enables per-trace-stream
Metadata indexes, index writes during trace ingestion, and indexed
time-range lookup. Only traces ingested after the feature is enabled are
indexed; there is no historical backfill. |
| `ZO_QUERY_TIMEOUT` | 600 seconds | Existing setting reused as the
cumulative Trace Timer locate/expand deadline and as the default timeout
for the trace-details search. |

When enabled, each traces stream can create a related Metadata stream
named `trace_time_index_{stream}`. Its retention settings follow the
source stream, and it is removed with the source stream.

## API changes

All timestamp parameters and returned time ranges use microseconds.

### Get a trace time range

```http
GET /api/{org_id}/{stream_name}/traces/{trace_id}/time_range
```

Query parameters:

- `hint_ts` (optional): timestamp hint used to start the index lookup

Returns the source stream, trace ID, and the resolved `start_time` /
`end_time`. The `range` field is `null` when the index cannot resolve
the trace.

### Get trace details

```http
GET /api/{org_id}/{stream_name}/traces/{trace_id}/details
```

Query parameters:

- `start_time` and `end_time` (optional, but must be supplied together):
caller-provided range
- `hint_ts` (optional): timestamp hint for the time-index lookup
- `timeout` (optional): trace-details search timeout in seconds

The endpoint unions the caller range with the indexed range, so it never
narrows a caller-provided range. If neither the caller range nor the
index can provide a range, it returns `400`. The response uses the
standard search response and includes the effective range in
`new_start_time` and `new_end_time`.

## Validation

- `cargo test -p openobserve-api-search --lib` — 145 passed
- `cargo test -p openobserve-core traces::time_index::tests --lib` — 7
passed
- `cargo test -p infra
db::trace_time_index::tests::test_coverage_start_cache -- --exact
--nocapture` — passed
- `cargo check -p openobserve-api-search`
- `cd web && npm run type-check:app`
- end-to-end ingestion and query validation with the data generator

## Validation notes

- targeted ESLint could not start because the local web dependencies are
missing `@eslint/css`
- enterprise-only compilation could not be completed from this OSS
checkout because the local enterprise overlay is unavailable
2026-08-14 15:29:23 +00:00
dbee572722 fix: slo bugs (#13784)
closes https://github.com/openobserve/o2-enterprise/issues/2397

---------

Signed-off-by: Yashodhan Joshi <yjdoc2@gmail.com>
Co-authored-by: Yashodhan Joshi <yjdoc2@gmail.com>
Co-authored-by: sai nikhil kethe <nikhil@openobserve.ai>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-14 10:31:32 +00:00
Omkar Kesarkhane 6927c2f335 fix(synthetics): collapse the variables panel by default (#13825)
## What

The variables panel on the browser-test **Journey** step now starts
collapsed.

## Why

The journey is what that page is for, and the panel was taking a share
of the width before the author had asked for it. The toolbar toggle sits
right next to the journey and is labelled, so bringing the panel in is
one click — and the splitter position is still shared with Configure, so
nothing about the panel's own behaviour changes.

## Scope

One line plus a comment. `variablesPanelOpen` is journey-only by design
(the existing comment above it explains why it is not shared with
Configure), so this cannot affect the Configure step.

```diff
-const variablesPanelOpen = ref(true);
+const variablesPanelOpen = ref(false);
```

## Testing

`npx vitest run src/views/synthetics/CreateBrowserTest.spec.ts` — 44
passing.

Split out of a larger synthetics branch so it can land on its own.
2026-08-14 09:43:24 +00:00
Harsh Mahajan 0ed5336bf1 chore: adding HA mode for o2-ai agent (#13525) 2026-08-14 16:14:03 +08:00
ktx-vaidehi e490a5c347 fix(logs): load the stream list on Visualize when no stream is selected (#13812)
Entering Visualize from the logs page with no stream selected left the
Stream dropdown on "No options found" — enabled, openable, and empty, so
there was no way to pick one.

PanelFieldList defers its initial fetch in edit mode until a stream is
set, exempting only "metrics". The Visualize tab ("build") needs the
same exemption: its select is the user's own, and the list is keyed on
stream_type alone, so waiting for a stream buys nothing.

Also arms the stream_type watcher at mount, so changing Stream Type with
a blank stream now reloads the list instead of doing nothing.

- https://github.com/openobserve/o2-enterprise/issues/2165
2026-08-13 13:01:21 +00:00
ktx-akshay d1f8941512 test: stabilize test cases for the Alpha environment (#13551) 2026-08-13 06:41:03 +00:00
Bhargav e4503f7fcb fix: Fix/match all pipe query corruption (#13764) 2026-08-12 08:50:33 +00:00
Amey PawarandHengfei Yang 3d06ed0423 fix(logs): reject LIMIT in filter mode instead of building an invalid query (#13773)
Closes #6915.

Without SQL mode the search bar's contents become the WHERE body of a
generated `select [FIELD_LIST][QUERY_FUNCTIONS] from "[INDEX_NAME]"
[WHERE_CLAUSE]`, and `handleNonSqlMode` splices the input in verbatim
after stripping comments and quoting known field names. Nothing checks
that the input is actually a predicate, so `k8s_namespace_name =
'nginx-ingress' LIMIT 10` produces valid SQL. The results grid runs it
and returns rows, while the histogram query built from the same input is
rejected by `is_eligible_for_histogram` and surfaces
`SearchHistogramNotAvailable`. That is the split described in the issue
— the log list works, the histogram errors.

The server side of this already exists, so the guard belongs in the UI,
before either request goes out.

## What changed

| | |
|---|---|
| `utils/query/nonSqlLimit.ts` | new — `hasLimitClause()` |
| `useSearchQuery.ts` | the guard in `handleNonSqlMode`, using the
file's existing `notificationMsg` + `return null` path |

Placement note: `hasLimitClause` lives in `utils/query/` rather than in
the composable because it holds no state and touches no reactivity — the
same split `doubleQuoteWarnings.ts` and `colorizeQuery.ts` already use.

The check is deliberately narrow. `LIMIT` matches only when followed by
a number, so `limit = 5` and `rate_limit >= 100` pass through. String
literals are blanked first, so `msg = 'rate limit 500 exceeded'` is
unaffected. Line and block comments are stripped after that, so `code =
200 -- limit 10` — which works today — keeps working.

## Tests

- **17** on the derivation — the issue's own query, case variants,
whitespace and newlines before the number, a bare `LIMIT 100`, a field
named `limit`, `rate_limit >= 100`, the word inside a literal, a literal
with a doubled quote, `limit = 'none'`, leading / trailing / block
comments, a real LIMIT alongside a comment, and a dash sequence inside a
literal.
- **6** on `buildSearch` — rejects the reported query and sets the
message, still builds without a LIMIT, allows `limit = 5`, ignores
commented-out LIMITs, and does not fire in SQL mode.

Each rejection test was checked to fail when the guard is reverted. The
negative cases stay green either way, which is what separates them from
tests that merely restate the implementation.

## Verification

- `npx vitest run --root src/ composables/useLogs utils/query` — **1965
passed**, 33 files, 0 failed
- `npx vue-tsc --noEmit -p tsconfig.vitest.json` — clean, and identical
to `main`
- `npx eslint` on all four files — clean
- `npx prettier --check` — clean

## Caveats

- Not exercised in a running app; covered by unit tests only.
- `ORDER BY`, `GROUP BY` and `HAVING` reach the same splice and are
**not** handled here. Happy to extend if you want them, but the issue
asked about `LIMIT` and I didn't want to widen it uninvited.
- `plugins/logs/IndexList.vue` holds an independent copy of the same
`[WHERE_CLAUSE]` splice for field-value lookups. It is reached by
clicking a field rather than running a search, so it sits outside this
path — flagging it rather than changing it.

---------

Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
2026-08-12 06:44:44 +00:00
Prabhat Sharma fb50aaea7f fix(deps): resolve 39 Dependabot alerts (#13774)
## Summary

- update vulnerable web transitive dependencies (`axios`,
`brace-expansion`, `fast-uri`, `http-proxy-middleware`, `js-yaml`,
`uuid`, and `websocket-driver`)
- remove the unused `node-polyfill-webpack-plugin`, eliminating the
unpatched `elliptic` dependency chain
- update Playwright to 1.55.1 and `node-forge` to 1.4.0 in the UI test
lockfile
- adapt `BaseImport` to Axios 1.18's stricter response-header type

This is expected to resolve 39 of the 41 currently open Dependabot
alerts.

## Remaining alerts

- `rmcp` remains on the project's custom `rmcp-openapi` fork. Updating
it safely requires a coordinated change to the enterprise
`Cargo.toml.openobserve`, which currently replaces the OSS manifest
during enterprise builds and pins the old API. The compatible
intermediate upstream releases also lack the fork's required
`allOf`/`anyOf` support.
- `thrift` 0.17.0 remains through `parquet` 58.4.0. The patched Thrift
line (0.23+) is outside Parquet 58's version constraint. Parquet 59
removes the Thrift dependency, but adopting it requires a coordinated
Arrow/DataFusion 59 upgrade.

## Validation

- `npm ci` and `npm audit --package-lock-only` in `web/` (0
vulnerabilities)
- `npm ci` and `npm audit --package-lock-only` in `tests/ui-testing/` (0
vulnerabilities)
- `npm run type-check:app`
- `npm run lint:ci -- --quiet`
- `npm run lint:design:strict`
- `NODE_OPTIONS=--max-old-space-size=8192 npm run build`
- `npm run test:unit -- --run` (41,917 passed, 404 skipped)
- `npx vitest run src/components/common/BaseImport.spec.ts --environment
jsdom` (41 passed)
- Playwright test discovery under 1.55.1 (2,076 tests in 265 files)
- `cargo fmt --all -- --check`
- `cargo test -p openobserve-mcp` (94 passed, 1 ignored)
- `cargo audit` (0 vulnerabilities; 2 allowed unmaintained warnings)
2026-08-11 22:17:35 +00:00
Prabhat Sharmaandgithub-actions[bot] 360b989272 feat(slos): PromQL support for count and time-sliced SLOs, plus SLO/alert regression fixes (#13761)
Supersedes #13738, whose commit is included here as the first of four.

Adds PromQL support to count and time-sliced SLOs, lets metrics streams
choose SQL or PromQL, and fixes three shipped bugs found along the way.

## Shipped bug: every SQL time-slice SLO recorded wrong history

`fetch_rows`' `SliQueryPlan::Single` arm normalized every hit with
`with_good_column: true`, but that arm serves **two** shapes and only
one projects `zo_slo_good`:

| shape | projects | result |
|---|---|---|
| count single-query | `zo_slo_good` + `zo_slo_value` | correct |
| time slice | `zo_slo_value` only | `good` defaulted to `0.0` |

So every SQL time-slice slice was classified from `0.0` instead of its
aggregate. With a positive threshold, `<`/`<=` (e.g. `p95 < 300ms`)
recorded every slice **good** — the SLO read ~100%, the budget never
burned, and no alert on it could fire. `>`/`>=` recorded every slice
**bad** and fired continuously. Shipped in v0.92.0 (`4772df1723`);
`to_row` had no time-slice test, which is how it got through.

Fixed by deriving the flag from the SLI type in a new
`single_query_rows`. Deliberately **not** fixed inside `to_row` by
falling back to `total` — that would read a count bucket whose `SUM`
came back NULL as *fully good*, inventing uptime. A test forbids it.

**Stored history is not rewritten.** It ages out within one window; an
operator can force a rebuild sooner by making a real edit (a no-op
re-save does nothing — `update` bumps only on `definition_changed`). No
auto-migration: bumping every time-slice SLO at boot would freeze every
attached alert org-wide until backfill cleared the coverage floor, and
double each org's row budget for 97 days — a self-inflicted monitoring
gap to repair a reporting error.

## PromQL support

**Time slice** carried a `query_language` field the planner ignored, so
a PromQL time-slice SLO saved cleanly and measured nothing, forever.

- `SliQueryPlan::PromQlValue` — one expression, not a good/total pair,
planned at slice ends from the same helper the count arm uses.
- The expression is passed through **verbatim**. Grouping comes from
series labels; wrapping it in `sum by (...)` the way the SQL arm injects
GROUP BY would destroy the aggregate (the sum of four pods' p95 is not a
p95).
- `promql_value_rows` **rejects** two series colliding on one
`(slice_start, group_key)` via `RejectReason::AmbiguousSeries` instead
of summing. Summing is right for counters (what `promql_rows` does) and
meaningless here. Detection is unconditional, so the ungrouped case — an
unaggregated `histogram_quantile` returning one series per instance — is
covered. Rejects are plumbed through `fetch_rows` into
`PassResult.rejected` on both the live and backfill paths.

**Count** already had a working `CountSource::PromQl` backend that no UI
could reach.

## Metrics streams choose SQL or PromQL

`language_suits_stream` refused SQL whenever `stream_type == "metrics"`,
and the form derived the language from the stream type — so metrics had
no SQL option. A metrics stream is an ordinary stream with a `value`
column, and `SELECT histogram(_timestamp, '5 minute'), avg(value) FROM
cpu_usage` is a perfectly good SLI. SQL is now valid for any stream
type; PromQL stays metrics-only. The form asks via a toggle (defaulting
to PromQL) instead of deriving.

## Validation

- `scope` refused for PromQL — a SQL `WHERE` fragment with nowhere to go
in a PromQL plan.
- Empty `query` refused for **both** languages (the SQL aggregate was
previously never validated — same silent-freeze shape).
- Expressions parsed with `promql-parser` at save, in `core` (`config`
has no parser).
- **Parse-only by design.** A grouping check requiring the root to be an
aggregation whose `by(...)` matches `group_by` wrongly rejects
`histogram_quantile(0.95, sum by (le, region)(...))` (function-call
root; `le` is a necessary label superset) and binary-op roots like `sum
by (region)(increase(a[5m])) - sum by (region)(increase(b[5m]))`. A
"warn instead" variant needs a warnings channel the save API doesn't
have.

## Also fixed

- **Time-slice comparator 422.** `SliConfig::TimeSlice.comparator` has
no serde default, so a dropdown the user never opened was omitted and
the save died with a bare 422 — blocking creation of *any* time-slice
SLO from the UI. Seeded to `<` only when blank, so a stored comparator
survives an edit.
- **SLO/alert/service-stream regressions** (the commit from #13738):
fractional-threshold serde round-trip, `SloAlertCondition` UI, alerts
table, service streams.

## Verification

- 487 config · 206 core · 45 infra · 418 frontend — all passing; clippy
`-D warnings`, `cargo fmt`, prettier, eslint, `vue-tsc` all clean.
- Driven through a real browser against a running server (isolated
instance, own data dir). All four SLI/language combinations create and
store correctly: count·PromQL, count·**SQL over metrics**,
time-slice·PromQL, time-slice·**SQL over metrics**. Also verified: the
toggle swaps field shapes both ways, scope shows for SQL and hides for
PromQL, PromQL-only hints appear only in PromQL mode, and editing a
stored SQL-over-metrics SLO reopens as SQL with its predicate intact.
- Validation rejections confirmed live with actionable messages:
PromQL-with-scope, unparseable PromQL, and an empty SQL aggregate.

Known limitation, pre-existing: `threshold` likewise has no serde
default and is not seeded, so leaving it blank still 422s. That is
genuine user error rather than an untouched-control trap; adding form
validation was out of scope.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-11 17:35:52 +00:00
Bhargav b07b3d4229 feat(functions): replace language radios with a segmented toggle (#13766)
The Add Function header selected VRL vs JavaScript with a radio group
and a single info icon. That icon only ever described the
ALREADY-SELECTED language, so learning what the other one was for meant
switching to it first.

- Radio group -> OFormToggleGroup segmented control with a sliding
indicator, each option carrying a language mark (V / JS). The badge
variants match the transType column in FunctionList so VRL/JS read the
same everywhere.
- Give EACH option its own info icon and static tooltip, so both
languages are readable without selecting either. Forced-language hosts
(workflow function nodes are JS-only) hide the toggle, so the locked
language's tip moves to a standalone icon.
- Add a pipe divider between the two options, and one between the
function-name title and the toggle.

data-test renames `...-vrl-radio` -> `...-vrl-option` (same for js):
"radio" described the old implementation. E2E page objects would have
broken on this change regardless of naming -- toggle items expose
data-state="on|off", not the radio group's "checked|unchecked".

Verified: FunctionsToolbar.spec.ts 18/18 pass, vue-tsc clean. ESLint
could not be run locally (@intlify/eslint-plugin-vue-i18n missing from
node_modules).
2026-08-11 13:25:45 +00:00
Omkar Kesarkhane 4b9204f69f feat(synthetics): collapse journey advisories into an on-demand suggestions chip (#13759)
## Problem

The browser journey editor stacked two **always-on** advisory cards
between the toolbar and the first step:

- *"This journey does not verify anything"* — no `assert` step in the
journey
- *"No test attributes found in this recording"* — no step produced a
`test_attribute` locator candidate

Both derived their visibility from the journey's own content, and both
conditions are the normal case for a real recording — so they rendered
on every visit to every affected journey. Each was dismissible, but
dismissal lived in a local `ref(false)`, so it died on remount: authors
closed the same two cards on every visit and still began reading their
steps two cards down the page.

## Change

One warning badge in the Steps toolbar — a warning icon and a count, no
prose:

- **Hover** names it ("1 suggestion for this journey"), from the same
string as the trigger's `aria-label` so the two cannot drift.
- **Click** opens a popover with each suggestion in full, separated by
rules, plus a footer stating that suggestions never block saving or
running.
- **Never opens on its own**, on any render.
- **No dismiss.** The chip costs no vertical space, so suppressing it
buys nothing — and a suggestion that can be waved away is one an author
never has to actually decline. A suggestion leaves when the condition
behind it is resolved.

A zero-assertion journey remains accepted with a warning; nothing here
blocks a save.

Transient banners are deliberately untouched — replay
running/passed/failed/stopped, incognito, pre-flight, recorder error.
They appear because something just happened, and a replay result that
does not announce itself is a worse product.

## Implementation

| File | |
|---|---|
| `web/src/utils/synthetics/journeySuggestions.ts` | new — pure
derivation over the steps; one small producer per rule; carries i18n
**keys** rather than text, so it needs neither Vue nor `useI18n` and a
rule is testable without mounting anything |
| `web/src/components/synthetics/journey/JourneySuggestions.vue` | new —
chip, tooltip, popover; presentation only |
| `web/src/components/synthetics/journey/BrowserJourney.vue` | computes
the suggestions, renders the chip in the toolbar, appends the offered
assertion |
| `ZeroAssertionNotice.vue`, `TestIdMisconfiguredNotice.vue` (+ specs) |
deleted — each was a visibility computation plus a card, and both halves
now have better homes |

Adding a future advisory is one producer and no template change.

Placement note: this lives in `utils/synthetics/` rather than a
composable because it holds no state and touches no reactivity — the
same split `deriveLocatorKind.ts` and `locatorStability.ts` already use
for derivations consumed by these components.

## Tests

- **17** on the derivation — both rules, empty journey, navigate-only
journey, empty candidate list, a test attribute outranked *within* a
step, both conditions at once in stable order, and that the offered
assertion is empty (`element_visible` with no selector/locator) rather
than guessed.
- **15** on the component — no chip at zero, count only and no prose,
pluralization, closed on mount, opens on click, tooltip suppressed while
open, action emits and closes, separators between suggestions.
- **5** on the `BrowserJourney` wiring — old hooks gone, suggestions
handed down, nothing offered when `readonly`, the assertion appended to
the tail with prior step order and ids intact, and the suggestion
dropping once the parent commits.

Two things the tests caught that review would not have:

1. Resolving the last suggestion unmounts the popover but not the
component holding its `open` flag — so a panel left open came *back*
open the next time a suggestion appeared. Regression test written,
confirmed failing, then fixed.
2. One component block runs against the **real** `OPopover` and `OBadge`
instead of a stub. `OBadge` declares `inheritAttrs: false` and the
popover trigger is `as-child` — precisely the pairing that silently
drops a click handler and leaves a chip that looks correct and does
nothing. Every other test stubs the popover and would not see it.

## Accessibility

`⚠ 1` has an accessible name of "1", so the trigger's `aria-label` is
load-bearing and carries the full count string. Child-mode tooltips are
pointer-only, so a keyboard user's path is that label plus the popover
they open with Enter.

Accepted trade-off, stated plainly: the old cards carried
`role="status"` and were announced on appearance. Nothing is announced
now — that announcement is what this change exists to suppress.

## Verification

- `npx vitest run src/components/synthetics src/utils/synthetics
src/views/synthetics` — 1147 passed, 58 files
- `npm run type-check` — clean
- `npm run lint` — clean for every touched file (the repo's pre-existing
`eslint.config.js` / `vitest.config.ts` findings are unchanged and
present on `main`)
2026-08-11 12:18:16 +00:00