Commit Graph
3106 Commits
Author SHA1 Message Date
dependabot[bot]andHengfei Yang 88051c7ec1 chore(deps): bump js-yaml from 4.3.0 to 4.3.1 in /web (#13678)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.3.0 to 4.3.1.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/nodeca/js-yaml/blob/4.3.1/CHANGELOG.md">js-yaml's
changelog</a>.</em></p>
<blockquote>
<h2>4.3.1 - 2026-07-31</h2>
<h3>Security</h3>
<ul>
<li>[backport] Remove quadratic complexity from <code>!!omap</code>
duplicate key detection.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/nodeca/js-yaml/commit/86e91b815b8794c3c73a179c1770871e37ec2df8"><code>86e91b8</code></a>
4.3.1 released</li>
<li><a
href="https://github.com/nodeca/js-yaml/commit/c3cc4b0bb9ddb9af2dd9b61e0d56f5ce7983cd4a"><code>c3cc4b0</code></a>
Backport quadratic complexity fix for !!omap</li>
<li>See full diff in <a
href="https://github.com/nodeca/js-yaml/compare/4.3.0...4.3.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=js-yaml&package-manager=npm_and_yarn&previous-version=4.3.0&new-version=4.3.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/openobserve/openobserve/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
2026-08-07 04:26:53 +00:00
dependabot[bot]andHengfei Yang 0b853837ca chore(deps-dev): bump postcss from 8.5.19 to 8.5.25 in /web (#13610)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.19 to
8.5.25.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/releases">postcss's
releases</a>.</em></p>
<blockquote>
<h2>8.5.25</h2>
<ul>
<li>Fixed 8.5.17 visitor regression.</li>
<li>Fixed <code>list.split()</code> for non-string values (by <a
href="https://github.com/amir-rezaei"><code>@​amir-rezaei</code></a>).</li>
</ul>
<h2>8.5.24</h2>
<ul>
<li>Preserve the BOM after the processing (by <a
href="https://github.com/hdimer"><code>@​hdimer</code></a>).</li>
</ul>
<h2>8.5.23</h2>
<ul>
<li>Do not load source map without <code>opts.from</code> for security
reasons.</li>
</ul>
<h2>8.5.22</h2>
<ul>
<li>Fixed custom property losing semicolon before a comment (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
</ul>
<h2>8.5.21</h2>
<ul>
<li>Fixed childless at-rule losing semicolon before comment (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed docs (by <a
href="https://github.com/isker"><code>@​isker</code></a>).</li>
</ul>
<h2>8.5.20</h2>
<ul>
<li>Fixed missing space if <code>AtRule#params</code> is set after (by
<a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed mixing AST error on warnings (by <a
href="https://github.com/MahinAnowar"><code>@​MahinAnowar</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/blob/main/CHANGELOG.md">postcss's
changelog</a>.</em></p>
<blockquote>
<h2>8.5.25</h2>
<ul>
<li>Fixed 8.5.17 visitor regression.</li>
<li>Fixed <code>list.split()</code> for non-string values (by <a
href="https://github.com/amir-rezaei"><code>@​amir-rezaei</code></a>).</li>
</ul>
<h2>8.5.24</h2>
<ul>
<li>Preserve the BOM after the processing (by <a
href="https://github.com/hdimer"><code>@​hdimer</code></a>).</li>
</ul>
<h2>8.5.23</h2>
<ul>
<li>Do not load source map without <code>opts.from</code> for security
reasons.</li>
</ul>
<h2>8.5.22</h2>
<ul>
<li>Fixed custom property losing semicolon before a comment (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
</ul>
<h2>8.5.21</h2>
<ul>
<li>Fixed childless at-rule losing semicolon before comment (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed docs (by <a
href="https://github.com/isker"><code>@​isker</code></a>).</li>
</ul>
<h2>8.5.20</h2>
<ul>
<li>Fixed missing space if <code>AtRule#params</code> is set after (by
<a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed mixing AST error on warnings (by <a
href="https://github.com/MahinAnowar"><code>@​MahinAnowar</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/postcss/postcss/commit/08c989c43cc87edb1ed71408c2f5164c54fc21df"><code>08c989c</code></a>
Release 8.5.25 version</li>
<li><a
href="https://github.com/postcss/postcss/commit/24f681471645cd960ee760ab7f9e348fbabfd42c"><code>24f6814</code></a>
Fix 8.5.17 visitor regression</li>
<li><a
href="https://github.com/postcss/postcss/commit/f2fa53f11daab3a16c7eb8bcaf5a945142341df3"><code>f2fa53f</code></a>
Add supply chain security requirement to PostCSS plugin guide</li>
<li><a
href="https://github.com/postcss/postcss/commit/10edf0b0606f97b1510e040c27bfd078c48d6ea7"><code>10edf0b</code></a>
fix: return empty array for empty string in list.split (<a
href="https://redirect.github.com/postcss/postcss/issues/2121">#2121</a>)</li>
<li><a
href="https://github.com/postcss/postcss/commit/0ebe8ad591621ab4e48311da47a76974617571f9"><code>0ebe8ad</code></a>
Release 8.5.24 version</li>
<li><a
href="https://github.com/postcss/postcss/commit/73218c64245be53e25d58150e0cc7e984f1d162d"><code>73218c6</code></a>
Update dependencies</li>
<li><a
href="https://github.com/postcss/postcss/commit/9a114f62b0deb37be859102f93b414b49385805a"><code>9a114f6</code></a>
Preserve the BOM when stringifying (<a
href="https://redirect.github.com/postcss/postcss/issues/2119">#2119</a>)</li>
<li><a
href="https://github.com/postcss/postcss/commit/90692619125cb9424f5eafd8c64bc76b2da23db1"><code>9069261</code></a>
Fix types check</li>
<li><a
href="https://github.com/postcss/postcss/commit/eb9e1fe793740bb3280bdf5bf98147f857f011bd"><code>eb9e1fe</code></a>
Release 8.5.23 version</li>
<li><a
href="https://github.com/postcss/postcss/commit/9d19c78ac91108b3f7d7130e55c6fa806c0efb84"><code>9d19c78</code></a>
Update dependencies</li>
<li>Additional commits viewable in <a
href="https://github.com/postcss/postcss/compare/8.5.19...8.5.25">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
2026-08-07 03:41:12 +00:00
dependabot[bot]andHengfei Yang a5c0249e65 chore(deps-dev): bump brace-expansion from 1.1.13 to 1.1.18 in /web (#13609)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion)
from 1.1.13 to 1.1.18.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/juliangruber/brace-expansion/releases">brace-expansion's
releases</a>.</em></p>
<blockquote>
<h2>v1.1.15</h2>
<ul>
<li>Backport v5.0.6 change to v1 (<a
href="https://redirect.github.com/juliangruber/brace-expansion/issues/111">#111</a>)
0b09384</li>
</ul>
<hr />
<p><a
href="https://github.com/juliangruber/brace-expansion/compare/v1.1.14...v1.1.15">https://github.com/juliangruber/brace-expansion/compare/v1.1.14...v1.1.15</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/juliangruber/brace-expansion/commit/758fcd6d188a95c2342818519c77b8c06794552b"><code>758fcd6</code></a>
1.1.18</li>
<li><a
href="https://github.com/juliangruber/brace-expansion/commit/27fbeed22b4fdf2c5f732f66bcf84d43f4a26c6e"><code>27fbeed</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/juliangruber/brace-expansion/commit/5c57cc2519dfb067e188b7cb0733fffbd02946bf"><code>5c57cc2</code></a>
1.1.17</li>
<li><a
href="https://github.com/juliangruber/brace-expansion/commit/d757f1dde7808bcbcd7a4628ab913e5185ed3d57"><code>d757f1d</code></a>
npm ignore <code>.claude</code></li>
<li><a
href="https://github.com/juliangruber/brace-expansion/commit/cb4b9e47cc2ec777c14b2b4492fb431a56f6a031"><code>cb4b9e4</code></a>
fix: backport GHSA-mh99-v99m-4gvg (<a
href="https://redirect.github.com/juliangruber/brace-expansion/issues/129">#129</a>)</li>
<li><a
href="https://github.com/juliangruber/brace-expansion/commit/447763a91a613cfa67ac73096cbc1de9a2304f97"><code>447763a</code></a>
1.1.16</li>
<li><a
href="https://github.com/juliangruber/brace-expansion/commit/d74e63030c012e3b7ae81657b8d665619cd51b95"><code>d74e630</code></a>
fix: v1 backport for CVE-2026-13149 (<a
href="https://redirect.github.com/juliangruber/brace-expansion/issues/122">#122</a>)</li>
<li><a
href="https://github.com/juliangruber/brace-expansion/commit/2203f4f4895eba16c4d408b4219ce1b8e5f6ff24"><code>2203f4f</code></a>
1.1.15</li>
<li><a
href="https://github.com/juliangruber/brace-expansion/commit/0b0938410732370559704230724ca4a44d1b29fd"><code>0b09384</code></a>
Backport v5.0.6 change to v1 (<a
href="https://redirect.github.com/juliangruber/brace-expansion/issues/111">#111</a>)</li>
<li><a
href="https://github.com/juliangruber/brace-expansion/commit/10c05fcf3699b1a29ef5e611c011af3d3c97e6e3"><code>10c05fc</code></a>
1.1.14</li>
<li>Additional commits viewable in <a
href="https://github.com/juliangruber/brace-expansion/compare/v1.1.13...v1.1.18">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
2026-08-07 03:28:18 +00:00
Ashish Kolhe bafc2be4f0 fix(correlation): P0/P1 fixes from service-streams correlation review (#13668)
## Summary

OSS half of the correlation P0/P1 fix set from the line-by-line review
(`docs/ak_review/correlation-line-by-line-review-2026-08-06.md`).
Companion enterprise PR: same branch name in o2-enterprise (required for
CI manifest pairing).

### Fixes (by review finding)

| Finding | Fix |
|---|---|
| **F14** | `StreamInfo.dropped_dimensions` contract field
(`#[serde(default, skip_serializing_if)]`, backward-compatible wire
format) + TS type |
| **F8** | Shared coverage-based identity-set ranking
(`resolve_best_set_by_coverage`) in `config::meta::correlation` — full
coverage > ratio > count, declaration-order ties; ingest and read now
rank identically |
| **F10** | `save_identity_config` purges rows of removed/changed sets
(`obsolete_set_ids`) + cache clear + org reload event |
| **F6** | `_reset` routes through enterprise
`ServiceStorage::delete_all` (cache clear + super-cluster replication)
and emits an org-scope reload; super-cluster Delete consumer emits org
reload instead of per-service delete |
| **F19** | `infra::table::service_streams::put`/`delete_subset_orphans`
return deleted orphan disambiguations so callers can evict cache keys |
| **F2** | `config::utils::sql::validate_where_fragment` (sqlparser)
applied to the `filter` param at both `traces/latest*` interpolation
sites — statement smuggling and comment-truncation payloads → 400 |
| **F2/F38** | FE: shared
`quoteSqlIdentifier`/`quoteSqlLiteral`/`buildSqlCondition` adopted by
all six SQL builders (two trace paths were fully unescaped) |
| **F35** | `applyFilterOverlay`: filter edits resolve per-stream via
semantic groups instead of literal key identity; dead
`removeFilter`/`resetFilters` removed; `semanticGroups` threaded to
`CorrelatedLogsTable` parents |
| **F36** | `applyDimensionEditsToFilters`: dimension-bar edits work in
both key spaces (raw-field and semantic-ID), case-insensitive shared
field→group map |
| **F31** | `mergeSubjectOverrides`: subject overrides replace the
same-group key (never union two aliases into contradictory WHERE),
schema-less field guessing removed |
| F14 (FE) | Warning banner shipped, then removed by reviewer decision
(`dropped_dimensions` stays in the API) |

### Verification

- `cargo test -p config` correlation/service_streams/sql: 121 tests
green; enterprise-feature and OSS-only cfg builds both compile
- FE: 263 vitest tests green across the correlation suites; vue-tsc
clean
- **Live-verified** against a local build (fresh DB): 19/19 API checks
(raw-value emission, set filing, obsolete-set purge, reset cache
invalidation, orphan eviction, filter validation) plus browser checks
(escaped SQL on the wire: `"environment" = 'Prod-EU'`)

Deliberately out of scope (per review decision): F15, F17/F37, F9.
2026-08-06 11:49:39 +00:00
Omkar KesarkhaneandClaude d171d98873 fix(synthetics): recorder extension install link, record/replay setup gate, configurable store URL (#13658)
## Summary

The OpenObserve Recorder Chrome extension is now published on the Chrome
Web Store. This PR wires the browser-test UI to it:

- **Install link + setup checklist** — the CreateBrowserTest gate links
to the Web Store listing and walks through install → Allow-in-Incognito
→ connect, with the connect step verified live via the extension probe.
The checklist is extracted into a shared `ExtensionSetupChecklist`
component.
- **Record/replay setup dialog** — `BrowserJourney.vue` now gates both
Record and Replay behind an `ExtensionSetupDialog` when the extension is
not detected (previously Replay was ungated and would hang in "running"
until the 15-min watchdog).
- **Loader on Record journey** — the gate's Record button shows a
spinner while the extension probe awaits its ~4.5s worst-case timeout.
- **Configurable store URL** — the Web Store URL is served by `/config`
as `synthetics_recorder_extension_url`, sourced from the new env var
`O2_SYNTHETICS_RECORDER_EXTENSION_URL`. The frontend falls back to the
built-in constant when the field is absent or empty, so the UI works
against older backends and OSS builds.

## Test plan

- `ExtensionSetupChecklist.spec.ts` (8), `ExtensionSetupDialog.spec.ts`
(11), `BrowserJourney.spec.ts` dialog gating tests — all passing
- `npm run lint` / `npm run type-check` clean (3 pre-existing
`eslint.config.js` duplicate-key errors untouched)
- Manual: record + replay gating verified against the published
extension

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-06 11:46:28 +00:00
Bhargav 1fd0bc5a32 fix: Fix/traces view logs url restore (#13662)
Two reports, same visible failure — clicking a "View Logs" affordance in
Traces either did nothing at all, or landed on the Logs page with no
stream selected, no filter, and the previous session's results still
showing:

Span-level "View Logs" in the trace-detail sidebar (enterprise).
"View Related Logs" on a service-graph node.
**Root cause**
Both trace back to one contract on the Logs side, hit by two different
callers.

The shared contract. restoreUrlQueryParams bails out entirely when the
stream param is absent
([useLogs.ts:343](vscode-webview://1natcp3amfqrfqrsn6p0sf7iek84lktqhb8p7td2pk978hr7cg9s/web/src/composables/useLogs.ts#L343)):


if (!queryParams.stream && queryParams.sql_mode !== "true") { return; }
That returns before applying the query, the time range and the stream —
so a missing stream doesn't degrade the navigation, it discards all of
it. Two further gates decide whether that function runs at all:
setupLogsTab() only restores when logs.isInitialized is false, and on a
keep-alive repeat visit handleActivation() restores only on the type ===
"trace_explorer" branch.

Trigger A — span "View Logs" (commit a7cbf9cfdd). viewSpanLogs read
correlationProps.value without awaiting loadCorrelation(). Correlation
is loaded lazily, so on a fast click that value was still null, and
navigateToCorrelatedLogs(null) threw on correlationProps.logStreams —
the button appeared dead. When the lookup did resolve but matched
nothing, logStreams was [], so the stream param came out empty and the
Logs page discarded everything per the contract above. The tree-view
handler had the same gap with a different failure mode: if
(correlationData) returned silently.

Trigger B — service graph "View Related Logs" (commit 1b02f512a1). The
stream was pushed under the wrong key — stream_value instead of stream —
so the early return fired every time. Two restore-path gaps compounded
it: isInitialized was left set (and isRouteChanged() clears it only on a
stream or org change, not a filter change), so setupLogsTab() skipped
the restore and re-showed the stored session; and the URL didn't carry
type=trace_explorer, so the keep-alive onActivated path had no branch
that would read it.

**Fix applied**
a7cbf9cfdd — fix(traces): warn when no correlated logs found

viewSpanLogs now awaits loadCorrelation() before reading the result.
Navigation is gated on correlationProps?.logStreams?.length — an empty
correlation result no longer navigates to an empty Logs page.
When there's nothing to open, the user gets a toast instead of silence.
A new correlationFailed ref separates "the lookup failed" (surface its
own reason) from "the lookup succeeded and matched nothing"
(traces.noCorrelatedLogsFound) — needed because correlationError is set
in both cases.
handleTreeViewCorrelatedLogs gets the same non-empty check and toast,
replacing the silent return.
Two sidebar specs that asserted the old navigateToLogs fallback now
assert the toast.
1b02f512a1 — fix(traces): restore logs URL params from service graph

stream_value → stream, so the restore no longer early-returns.
Added type: "trace_explorer" so the keep-alive onActivated path restores
on repeat visits.
Explicit store.dispatch("logs/setIsInitialized", false) so the URL is
the source of truth rather than the stored session.
Spec coverage for both paths in ServiceGraphNodeSidePanel.spec.ts.
2026-08-06 10:30:32 +00:00
Ashish Kolhe 32d02d6781 chore(traces): make Service Graph and Services their own routes (#13659)
## What

Service Graph and Services Catalog were tabs on the Traces page,
switched via a `?tab=` query param on a single keep-alive route. They
are now real routes, reached from a hover flyout on the Traces rail
tile:

| Route | Notes |
|---|---|
| `/traces` | Traces search — keeps the **Spans / Traces** toggle |
| `/traces/service-graph` | enterprise-gated; redirects to Traces on OSS
|
| `/traces/services` | |

**Spans stays an in-page toggle** on purpose. It's a view *granularity*
that shares the query, stream and time range — you switch to it
mid-investigation and expect context to carry over. Datadog's Trace
Explorer does the same. Service Graph and Services are destinations;
Datadog, New Relic and Grafana all treat them that way.

## Why this wasn't just cosmetic

As tabs, all three views shared one `meta.title`, one history entry, and
the traces page's `searchObj`. That last part is why Service Graph and
Services Catalog inherited the traces page's selected stream — the
coupling this PR removes structurally.

## UI

Both pages now use the Agent Graph header pattern (`OPageLayout` +
title/subtitle/icon, date picker and last-run refresh in `#actions`),
with a subnav row carrying stream selector + search on the left and
view-type + layout on the right.

## Handoff

`view-traces` no longer mutates the shared store from a sibling tab.
Both views build the same filter string — extracted verbatim into
`viewTracesHandoff.ts` — and navigate to Traces with it as query params,
so the URL survives a reload. (Traces then canonicalises `filter=` to
its own base64 `query=` via existing code in `useTraces.ts:290`.)

## Two bugs this surfaced

Both were things the tab views inherited for free and now have to set
themselves — and both would have hit real users deep-linking these
pages:

- **`organizationIdentifier`** was only assigned inside the Traces
page's `loadPageData()`. Without it the catalog issued requests to
`/api//_search_stream` (empty org segment) and rendered nothing.
- **`?period=` / `?from=`+`?to=`** were only applied by the Traces
page's `restoreUrlQueryParams()`. Without it a shared link carrying a
time range silently opened on the 15-minute default.

## Tests

| Suite | Result |
|---|---|
| `Traces/service-graph.spec.js` | 14 / 14 |
| `Traces/service-catalog.spec.js` | 10 / 10 |
| `RegressionSet/Traces/traces-ent.spec.js` | 1 / 1 |
| Unit (`Navbar` + `plugins/traces`) | 2514 pass |

Also clean: `type-check`, `eslint` (0 errors), `lint:design:strict`.

Test-side changes: navigation POMs go through the rail flyout instead of
the removed toggles; the graph POM pins an explicit window, since on its
own route the graph no longer inherits the traces page's range. The
shared auth fixture now verifies against `navbar-main-nav` — it keyed
off `menu-link-/-item`, which the current rail no longer renders, so
suites failed setup even after a successful login.

Filter-building coverage moved with the code into
`viewTracesHandoff.spec.ts` (21 tests) rather than being deleted
alongside the component tests that asserted the old in-page handlers.

## Known / follow-up

- `RegressionSet/Traces/traces-regression.spec.js` has 2 failures (trace
column sorting, pagination). **Pre-existing** — they fail identically
against production on `main`, and the Service Catalog test in that file
passes.
- Both views still default their stream picker to the traces page's
selected stream. Defaulting Service Graph and Services to **all
streams** is a deliberate follow-up: on our own instance the `default`
stream shows 22 topology nodes vs 71 across all streams, and services
like `o2-ai` span 4 streams. Graph is a one-line change (the API already
supports it); the catalog needs a backend decision, so it's out of scope
here.
2026-08-06 10:30:03 +00:00
Ashish Kolhe bb6e6f3f5e fix: preserve raw payload when switching template mode tabs (#13661)
Fixes openobserve/o2-enterprise#2364 (P0/Blocker regression in
v0.92.0-rc3, introduced by #13640).

## Bug

On an **existing custom (raw-payload) template** — which is every
template saved before templates v2 — the two editor-mode tabs share the
single form `body` field, and `onModeChange` overwrote it destructively:

1. Open existing template → `contentSpec` initializes to
`emptyContentSpec()`.
2. Click **Template** tab → `body` is overwritten with the serialized
*empty* spec → tab renders blank, original payload destroyed in form
state.
3. Click **Raw payload** back → nothing restores it → the editor shows
empty-ContentSpec JSON ("raw payload resets").
4. Worse: hitting **Save** on step 2 persisted the empty spec over the
stored template — silent data loss.

The banner path ("Start a content version") had the same latent
overwrite via the `contentSpec` watcher.

## Fix

- Stash the raw body when leaving custom mode; restore it (including
unsaved raw-mode edits) when returning. The stash takes priority over
the new-template starter payload.
- When an existing custom template enters content mode with a
still-empty spec, prefill the spec body from the payload's detected
`{vars}` (same logic as `startContentVersion`) so the Template tab never
opens blank.
- `startContentVersion` stashes too.
- `applyTemplateData` clears the stash so the kept-alive component never
restores a previous template's payload.

## Tests

4 new regression tests (TDD — all watched failing first with the exact
reported symptoms):
- Template → Raw payload round trip restores the original payload
- unsaved raw-mode edits survive the round trip
- Template tab prefills from detected `{vars}` instead of opening blank
- "Start a content version" → Raw payload restores the original payload
too

`AddTemplate.spec.ts` 42/42 green; neighboring template specs (151
tests) green; ESLint, `vue-tsc`, Prettier clean.
2026-08-06 10:17:51 +00:00
Ashish Kolhe 58c7dafa55 fix: notification templates v2 — rich content templates + alert chart images (#13640)
## Summary

Two coupled alerting features:

**1. Notification Templates v2 ("content" templates)**
- New `kind: content` template type: a structured spec (title, body
markdown, fields, links, rows, per-channel title overrides,
severity-filtered visibility) rendered natively per channel — existing
`custom` (raw body) templates are untouched and byte-identical on the
send path.
- Per-channel renderers: Slack Block Kit, Teams (Adaptive Card +
MessageCard), Discord embeds, PagerDuty Events v2, Opsgenie, ServiceNow,
SNS, HTML/text email, and a stable machine `webhook` envelope.
- Escaping is renderer-owned (mrkdwn entities, HTML encoding, markdown
stripping) with a shared URL-scheme allowlist; golden-fixture tests per
channel.
- Template editor UI: guided content form (fields/links row tables,
compact rows config, channel title overrides), live preview panel with
per-channel preview + payload tab, test-send (rate-limited,
`[TEST]`-marked).
- Migration `m20260802_000001_add_template_kind`; prebuilt default
template + org backfill.

**2. Alert chart images (stateless, storage-free)**
- Alerts can attach a metric-history chart PNG rendered from the
triggers self-reporting stream (`actual_value` per evaluation), with
warn/crit threshold lines and the firing point appended.
- Zero storage: the chart URL *is* the data — deflate+base64url payload
signed with truncated HMAC-SHA256 (org-bound, expiry embedded), rendered
on fetch by the HTTP-terminating node (`GET
/api/v2/{org}/alerts/charts/render`). Signature verified before inflate
(decompression-bomb guard), uniform 404s, per-node render semaphore +
per-minute query budget + small LRU.
- Renderer: plotters + embedded DejaVu Sans (Bitstream Vera license
included), gap-broken line segments so sparse evaluation history doesn't
draw false plateaus.
- Channel delivery: Slack image block, email inline CID (no remote
fetch), Discord multipart attachment, Teams/PagerDuty image URLs;
per-destination opt-in via template chart flag.
- Slack layout live-A/B'd on a real channel: content + buttons/chart
split across two same-colored attachments so the severity stripe spans
everything while staying under Slack's "Show more" fold; clickable link
title; "View in OpenObserve" keeps a reserved primary-button slot.
- Slack image-delivery fallback: Slack validates image URLs server-side
and rejects the ENTIRE message with `400 invalid_attachments` when its
proxy can't fetch the chart (VPN-only/private `ZO_WEB_URL`) —
live-verified. On that rejection the send strips the image blocks and
resends once (alert always delivers), sets a process-wide flag so later
sends pre-strip (reachability is deployment-wide, not per-destination),
and re-probes hourly. Zero config; observable via `slack_image_rejected`
/ `slack_image_pre_stripped` metric events.

## Testing
- 147 notification unit tests (resolve/render/preview/test-send incl.
per-channel goldens and hostile-input/injection coverage), 8 chart
payload tests, renderer PNG tests.
- Live-verified end to end: Slack (Slackbot chart fetches ~100ms,
cache-immutable semantics), SES email inline image, multi-severity
firing scenario (warning→critical) with correct threshold lines.
- FE: AddTemplate/ContentTemplateForm/preview specs; UX audit pass with
fixes.
- `cargo clippy -D warnings` clean in both OSS and enterprise
arrangements.

## Enterprise pairing
Must merge together with openobserve/o2-enterprise#2353 (workspace
twin-manifest mirror for the new OSS deps).
2026-08-06 04:47:02 +00:00
Shrinath Rao c9f68754df fix(logstream): show local timezone in stream detail time range (#13652)
## Problem

The **Stream Detail** header shows the stream's document time range with
a timezone badge that is **hardcoded to \`UTC\`** (i18n key
\`logStream.utc\`), while the timestamps next to it are rendered by
date-fns \`format()\` in the **browser's local timezone**. So the badge
and the value disagree for any non-UTC user — the range reads as local
time but is mislabeled "UTC".

## Fix

- Add \`formatTimestampInTimezone(us, format, timezone)\` in
\`web/src/utils/date.ts\` — same as \`formatTimestamp\` but renders in a
given IANA zone (via \`date-fns-tz\` \`formatInTimeZone\`, already a
dependency), matching the pattern already used across dashboards.
- In \`schema.vue\`, add a \`displayTimezone\` computed =
\`store.state.timezone || <browser zone>\` (never a hardcoded "UTC"),
render it in the badge, and format the \`doc_time_min\`/\`doc_time_max\`
range in that same timezone.

Now the badge label and the displayed range always use the same timezone
— the user's selected timezone, falling back to their browser zone.

## Scope note

The separate **Extended Retention** date picker further down still uses
UTC intentionally (retention day-boundaries are computed in UTC on the
backend), so its \`(UTC Timezone)\` label is left unchanged.

## Validation

Verified \`formatInTimeZone\` behavior against the installed
\`date-fns-tz@3.2.0\`:

| timezone | rendered value |
|---|---|
| UTC | \`2025-08-03T20:03:22:99\` |
| Asia/Kolkata | \`2025-08-04T01:33:22:99\` |
| America/New_York | \`2025-08-03T16:03:22:99\` |

Label and value now agree in every case.
2026-08-06 04:26:06 +00:00
Shrinath Rao 13acdd61da fix(web): hide LLM Model Pricing settings on OSS builds (#13651)
## What

Hide **LLM Model Pricing** on OSS builds and make it an
enterprise/cloud-only feature, end to end (UI + e2e coverage). Paired
with o2-enterprise PR on branch `fix/hide-llm-model-pricing-oss`.

## Why

Model Pricing was surfaced on OSS (settings rail + navbar flyout), gated
only on the `model_pricing_enabled` backend flag with **no**
enterprise/cloud check — unlike its sibling AI feature **LLM Providers**
(`(isEnterprise || isCloud) && online_evals_enabled`). Product decision:
it should not appear on OSS. This brings it in line with that sibling.

## Changes

**Web (hide on OSS)**
- `web/src/components/settings/index.vue` — settings rail item now
`visible: (isEnt || isCloud) && !!z.model_pricing_enabled`.
- `web/src/lib/core/Navbar/navGroups.ts` — `modelPricing` gate predicate
now requires enterprise/cloud (mirrors `llmProviders`), so the
collapsed-navbar flyout matches the rail.
- `web/src/composables/shared/useManagementRoutes.ts` — `model_pricing`
+ `model_pricing/edit` routes registered only inside the `isEnterprise
|| isCloud` block (defense-in-depth; matches `llm_providers`), so the
route isn't reachable by direct URL on OSS.

**E2E (move coverage to ENT)**
- `tests/ui-testing/ci-matrix/ci_matrix.json` — drop
`model-pricing.spec.js` from the OSS `GeneralTests` shard. It's added
back on ENT via the enterprise overlay (`ci_matrix.ent.json`, in the
paired ENT PR), so the merged ENT matrix runs it exactly once (verified
with `build-ci-matrix.js`).
- `.github/workflows/playwright.yml` — remove `ZO_MODEL_PRICING_ENABLED:
true` (OSS no longer exercises the feature; ENT `playwright.yml` keeps
it).
-
`tests/ui-testing/playwright-tests/GeneralTests/settings-form-validation.spec.js`
— gate the *ModelPricingEditor* describe block to a clean SKIP on OSS,
mirroring the existing Domain Management / Org Storage / Correlation
blocks.
- `tests/ui-testing/pages/generalPages/settingsFormValidationPage.js` —
`navigateToModelPricing()` now returns a boolean so the suite can detect
the OSS build and skip (mirrors `navigateToDomainManagement()`).

## Not changed (deliberately)

The backend `ZO_MODEL_PRICING_ENABLED` default (`true`,
[config.rs](src/config/src/config.rs)) is **left as-is**. It lives in
the shared config crate used by both editions, so flipping it to `false`
would also regress real ENT/Cloud deployments (feature off by default).
The UI gate already ensures the page never shows on OSS regardless of
the flag. Making the backend itself enterprise-only would be a separate,
backend-owned change.

## Behavior

| Build | Model Pricing UI | e2e |
|-------|------------------|-----|
| OSS | Hidden | `model-pricing.spec.js` not run;
settings-form-validation block SKIPPED |
| Enterprise / Cloud | Shown (if flag set) — unchanged |
`model-pricing.spec.js` runs (via ENT overlay) |
2026-08-06 01:54:37 +00:00
Abhay Padamani 7888f5c5fd feat: add hyperlink support for URL cells in (#13650)
Design At: hyperlink support for dashboard table cell if value is link
2026-08-05 12:57:48 +00:00
Bhargav bbf2aa37af feat: Issue ent 2289 mobile rum sdk error tracking v2 (#13645)
Design at: designs/tree/main/mobile-rum-sdk-evaluation/design-docs
2026-08-05 12:18:13 +00:00
Omkar Kesarkhane a24cd706af fix: Updated synthetics locations UI in configure location (#13475)
Redesigns the **check-locations picker** (`CheckLocations.vue`) used by
the browser-test and protocol-check create/edit forms, and tightens how
the two parent views feed it.

## Locations picker (`CheckLocations.vue`)

**Finding & choosing locations**

- Search box (matches label and region) with an explicit **Refresh**
button; the row stays mounted during loading (disabled input + spinner)
so a refresh never flickers the UI away.
- Location options render **two per row** (`grid-cols-2`, collapsing to
one column on narrow viewports), with public and private sections
labeled and explained via info-icon tooltips.
- Loading shows skeleton rows in place of the list.

**Private-location status at a glance**

- Each private location resolves to a status tier — **Ready / Connecting
/ Offline / Down** (down = no agent for ≥24h) — shown as an icon +
outline badge per row.
- Not-ready rows carry an always-visible guidance line (tooltip-only
messaging was unreachable on touch/keyboard) and a per-row **Add agent /
Install agent** action that opens the agent-setup drawer pre-scoped to
that location.
- Live agent count with first agent name inline; additional agents
collapse into a `+N` badge with a tooltip listing all names.
- Rows sort by resolved tier (ready → connecting → offline → down,
alphabetical within a tier), so a dead location never outranks one that
dropped minutes ago.

**Empty & no-match states**

- Search miss in one section → section keeps its header and shows "No
public/private locations match your search".
- Search miss in both sections → the list collapses to a single inline
empty state with a **New private location** action (omitted for browser
tests, which are Lambda-only).
- The private section's creation empty state (with CTA) appears only
when the org truly has no private locations — never as a filter
artifact. It remains reachable in a zero-locations org, since it is the
only way to bootstrap one.

## Parent views (`CreateBrowserTest`, `CreateProtocolCheck`)

- Agent-setup drawer is now **location-scoped**: `add-agent` passes the
clicked location's id/name through; `new-location` opens it unscoped.
Scope clears on close.
- No auto-refetch of locations on drawer close — the explicit Refresh
button covers it, so dismissing the drawer no longer triggers a
pointless fetch and skeleton flash.
- Unified `fetchLocations` error handling: silent when the endpoint 403s
(not available on this build), error toast on real failures. Previously
the browser view swallowed every failure and the protocol view toasted
every failure — including the expected 403.

## Cleanup

- Removed i18n keys orphaned by the rewrite (`privateBadge`,
`setupAgent`, `offlineWarning`, `liveAgentsNamed`) from all 15 locale
files, plus never-wired keys (`viewAgents`, `hideAgents`,
`lastAgentSeen`, `summaryNoneHealthy/Mixed/AllHealthy`) from en-US. The
synthetic-monitoring section's own `setupAgent` key is untouched.

## Testing

- `CheckLocations.spec.ts`: 48 tests covering search filtering,
per-section and combined no-match states, status tiers, guidance
visibility, agent display, tier-aware sorting (including a case where an
alphabetical tie-break would give the wrong order), loading, and empty
states. Also fixes a stub bug where `OButtonStub` declared `emits:
["click"]` and silently swallowed every click, letting a hollow test
pass.
- Both view specs cover the 403-silent / toast-on-failure split.
- `npm run lint` and `npm run type-check` clean on all touched files.
2026-08-05 11:58:08 +00:00
ktx-vaidehi dafc1ce7f7 feat(ui): dashboard query editor redesign (#13637) 2026-08-05 11:17:44 +00:00
ktx-vaidehiandKirtan Thakkar 3285a31e6f fix: i18n lint enforcement (#13439)
Co-authored-by: Kirtan Thakkar <kirtan@kiara.tech>
2026-08-05 10:16:00 +00:00
ktx-akshay 95e1ee83c3 fix(search): improve loading behavior on stream change (#13636) 2026-08-05 09:33:35 +00:00
ktx-vaidehi 6e870594f4 fix: logs page freezes when wrap is enabled (#13638)
Every virtual row called the virtualizer's global measure() from
onMounted whenever row heights could vary, which includes wrap mode.
measure() throws away the entire itemSizeCache, so with wrap on each
mounting row wiped all measured heights, the rows fell back to the
estimate, the ResizeObserver reported the real (taller) heights, offsets
shifted, more rows mounted, and the cycle repeated — "Maximum recursive
updates exceeded in <OTable>" plus ResizeObserver loops until the tab
locked up.

Fixed-height mode converges instantly (the re-measure returns the same
constant), which is why only wrap froze.

Skip the global measure() in dynamic-row-height mode: measureRowElement
already hands each <tr> to the virtualizer, which measures and observes
it individually. The expanded-row path (wrap off) is unchanged.

Measured on a 1000-row table with the logs table's props, scrolling one
viewport with wrap on: 1 rAF frame in 3s and a 4260ms worst frame gap
before, 179 frames and 51ms after — matching wrap-off (180 frames /
42ms).
2026-08-05 09:20:15 +00:00
hengfei yang e525ad4b17 ci: fix windows build 2026-08-05 08:56:09 +08:00
Hengfei Yang a05eba7ae0 ci: fix build on window (#13631) 2026-08-05 08:42:16 +08:00
ktx-vaidehi 5525fa48f1 fix(alerts): bring External Alert Sources page up to the listing standard (#13625)
## What

Brings the **External Alert Sources** page in line with the house
listing-page standard (`ui-architect` skill + `page-recipes`), following
the patterns already used by `SyntheticsTokens.vue` and
`AlertsDestinationList.vue`.

### List page — `ExternalAlertSourcesList.vue`

- **Layout**: `OPageLayout scroll pad-y` → `bleed` + the full-height
flush chain, so the table scrolls internally instead of the whole page.
- **Toolbar** (all three were missing): search box in `#toolbar`,
refresh moved from the page header into `#toolbar-trailing`, and the
column show/hide toggle (via `persist-columns` + `table-id` + `hideable`
columns).
- **Empty state**: the table was hidden entirely behind
`v-if="defaultSource"`, so a fresh org saw a blank page. It now always
renders, with one `OEmptyState` (new `no-alert-sources` preset) that
swaps to "no results / clear filters" via `:filtered`.
- `:loading` bound (no skeleton before), footer row count via
`:footer-title`, and `n` / `r` / `/` shortcuts registered + bound.
- **Name column resize**: it used `meta.autoWidth`, which `useTableCore`
marks `enableResizing: false`, so the Name│Status boundary had no drag
handle. Switched to `meta.flex` (same recipe as the synthetics
`MonitorTable`) — still absorbs leftover width, but resizable.
- Removed a hardcoded `⚠` glyph from the no-destination tag.

### Add/Edit drawer — `AddExternalAlertSource.vue`

- Converted the hand-rolled `form` object + disabled-Save validation to
`OForm` + a colocated zod schema, with fields addressed by `name=` only
and wired to the drawer footer via `form-id`. Save is no longer disabled
on invalid and the manual `saving` flag is gone.
- `form-id` is dropped once the source exists, so the "Done" button
closes instead of resubmitting.
- Select width moved to a wrapper (`OSelect` self-applies `w-full`) and
its hand-rolled `<p>` label became the `label` prop.

## Testing

- `ExternalAlertSourcesList.spec.ts` — 38 pass (8 new: search filtering,
empty state, clear-filters, footer count, resize handles). Binding
`:loading` made DOM assertions hit OTable's minimum-hold skeleton, so
they now go through a `mountAndSettle()` helper.
- `AddExternalAlertSource.spec.ts` — 22 pass, rewritten off `vm.form`
onto the validated-values API, plus two tests that submit the real
`<form>` end-to-end.
- Full `src/components/alerts/` suite green (1997 tests). `npm run
type-check`, ESLint and `lint:design:strict` all clean.
2026-08-04 13:31:30 +00:00
ktx-vaidehi 2a68900e12 feat(ui): dashboard tab reorder rename (#13572) 2026-08-04 12:43:55 +00:00
ktx-vaidehi 601ef75e2e fix: IAM page UI changes (#13618) 2026-08-04 12:43:36 +00:00
Sai Nikhil 375a7360de fix: alert v4 issues (#13601)
This pr fixes the issue on #13571
2026-08-04 10:15:49 +00:00
Sai Nikhil 232b38351d fix: search history routes change (#13512) 2026-08-04 09:37:20 +00:00
Sai Nikhil 76f1df06fa fix: alert timezone fix on main (#13519) 2026-08-04 09:36:56 +00:00
Omkar Kesarkhane 1ec3b5d0dc fix(synthetics): make journey validation errors reach the step they name (#13616)
## Problem

Saving a browser check with an invalid step showed only the toast **"Fix
the highlighted fields before saving"** — no step expanded, no field
highlighted. Two separate faults produced that symptom.

### 1. In create mode, nothing reached the journey at all

`OStepper` runs in **wizard mode** here (no `expanded` prop, default
`false`), so `OStep` renders its panel behind `v-if="isActive"` —
**`BrowserJourney` exists only while `currentStep === 1`**. Create
mode's only Save button lives in the `currentStep === 2` footer, so
`journeyRef.value` was `undefined` when `persist()` ran:

```ts
currentStep.value = 1;                                        // not flushed yet
journeyRef.value?.validateStepSelectors?.();                  // no-op
journeyRef.value?.setStepFieldErrors?.(result.error.issues);  // no-op
toast({ variant: "error", … });                               // the only survivor
```

A `nextTick` would not have been enough: a **freshly mounted**
`BrowserJourney` starts with an empty error map, so an imperative push
cannot survive the remount. Edit mode worked because it has a Save on
the Journey step — which is why this read as create-mode-only.

### 2. Even when they arrived, only 2 of 7 rules expanded anything

Auto-expand lives in `validateJourneySteps()`, which knows two rules.
The other five — `stepNameRequired`, `retiredAction`, the navigate URL,
`typeTextRequired`, `expectedRequired` — live in the zod schema and
reached the journey through `setStepFieldErrors()`, which recorded the
message and returned. `getRowStatusColor()` ignored them too, so
"highlighted" was literally false for those rules.


## Fix

- **Issues are parent-owned state passed as the `fieldIssues` prop**,
applied by an `immediate` watcher. Mount order stops mattering — issues
raised while the journey is unmounted wait, and rows open whenever it
next renders (including the Details-error + journey-error case, where
the author lands on Configure and finds the journey already expanded on
switching tabs).
- **Both paths route through `revealErroredSteps()`** — opens every
errored row in journey order, clears an active filter (a filtered-out
row isn't rendered, so expanding it shows nothing), scrolls to the
first. `getRowStatusColor()` reads schema errors too.
- **Stale errors clear as the author edits.** The editor emits no
per-field event for name/value/expected, so those stayed red over
corrected input and kept re-opening a correct row.
- **Stale errors clear on a successful save** — `setStepFieldErrors` was
only ever called on the failure branch.
- **Duplicate error toast gone.** `persist()` no longer calls Path A at
all; the schema already emits both of its rules as field issues.

Removed as dead: `setStepFieldErrors` from `defineExpose`, and the
`silent` option added mid-fix for the duplicate toast.

## Tests

846 passing. Validation had large untested surfaces — which is how
coverage decayed without a single failure. Closed here:

- monitor-level `name`/`url`/`locations` and the **entire gate schema**,
neither ever exercised (`form()` supplies valid values; the view spec
replaces the schema module with a stub)
- `validateJourneySteps`' auto-expand — **untested since the first
commit**
- the `expectedErrorMessage` binding, `.trim()` on type text and
expected, the `\S+$` part of the navigate regex, `element_not_visible` /
`page_title` / `element_attribute`, and the `scroll`/`screenshot`
retired actions

The view spec's `OStep` stub was `v-if="$parent.$parent || true"` — it
rendered **every** panel, keeping `journeyRef` alive on the Configure
step, which is why this spec could not have caught fault 1. It now
mirrors wizard mode, and a create-mode save with a journey error is
asserted end to end.

**Every rule verified by mutation** — disabled at source, suite re-run,
reverted. **33 mutations, all caught.** Three initially ran green and
led to stronger tests: `min(1)` on `url` only governs *which message*
appears (the protocol refine rejects `""` anyway), nothing reached
`\S+$`, and `validateJourneySteps`' expand had no test at all.

## Not fixed — filed for follow-up

Found in the same sweep, deliberately out of scope:

- **`select` / `press` / `upload` accept a blank value.** `v2Value()`
maps all of them to the wire, so they save, run, and **pass** — the
false-green `typeTextRequired` exists to prevent. `type` isn't special;
it just got a rule.
- **A zero-step journey saves.** No `.min(1)`, and the first-step check
treats an empty array as valid.
- **`element_attribute` has no attribute rule**, and no error binding
for that field.
- **`timeout` / `settle.budget_ms` warn but never block.** Needs
`Advanced` to become controlled first, or the rules land inside a
collapsed section and reproduce this same bug one level deeper.
- **Continue and Replay use the 2-rule gate**, not the schema — so a
journey with a blank type text replays and false-greens.
- **A whitespace-only monitor name is accepted** (`.min(1).trim()` vs
the step rule's `.trim().min(1)`), so a check named three spaces saves
and displays blank.

Full write-up with the regression timeline, the mutation table, and why
each layer's tests stayed green is in
`docs/synthetics/002-journey-step-validation-audit.md` (untracked —
`docs/` is gitignored in this repo).

## Verification

- `npx vitest run src/components/synthetics src/views/synthetics` → 43
files, **846 passed**
- `npm run type-check` → clean
- `npx eslint` on changed files → 0 errors (4 pre-existing warnings,
none introduced)
- `npx prettier --check` → clean
2026-08-04 08:27:36 +00:00
6f0b2dcb07 fix: workflow history (#13588)
This enhances the workflow history , stores all runs instead of just
failed ones and also some UX improvements on FE

---------

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>
2026-08-04 08:22:01 +00:00
Ashish Kolhe b31ff6c7fe chore: incident destinations, source rename, and Alert Sources UX redesign (#13612)
## Summary
- Adds a typed `destinations` field to incident_integrations (migration
+ entity + table ops), threaded through create/list/event-ingestion.
- Adds a PATCH endpoint to rename an alert source, including the org's
`default` source (rename allowed; delete stays blocked for it).
- Moves Alert Sources under the Reliability nav menu.
- Redesigns the Alert Sources list and Add/Edit drawer:
- Table-only landing page; Add Source opens a drawer instead of a route.
- Single row-level Edit action (name + destinations together), matching
the Destinations/Templates list pattern instead of per-field popovers.
- Fixed-layout, horizontally scrollable table with a pinned Actions
column; wrapped/truncated cells confined to their own columns.
  - Separate "copy token" action distinct from "copy URL".
- "Never resolves" warning moved from an unbounded banner list into a
tooltip on the status tag/icon.
- Removed the confusing "Shared token" badge and reworded the
no-senders-yet caption.
- All 3 setup steps shown together in the drawer (checklist-style)
instead of hiding steps 2-3 behind a post-save view.
- Destination creation now routes to the real Destinations page instead
of a fake local-only option.
- Fixed the test curl snippet: no stray `#` comment in the copyable
text, and a payload the backend's generic-format detector actually
accepts.

## Test plan
- `cargo build`/`clippy -D warnings` clean on `infra`,
`openobserve-api-management`, `openobserve-api-http` (enterprise
feature) and OSS-only workspace check.
- 151 frontend vitest tests passing (stable across repeated runs).
- Manually verified against a local enterprise build + Vite dev server.
2026-08-04 05:16:32 +00:00
Prabhat Sharma 624425a08a feat(editor): rebuild query autocomplete, and fix the value path behind it (#13586)
Design at autocomplete/autocomplete.md

Rewrites the query editor's autocomplete across every surface that has
one, and
follows the field-value path far enough to fix the bugs it was hiding.
53
commits, in six phases; each phase was written test-first, reviewed
adversarially, and checked in a browser before the next one started.

## What was wrong

The completion providers were wired inconsistently. Six surfaces bound a
resolver that was never defined, so their value completion could not
resolve at
all — SLO, dashboards and anomaly detection among them. Traces never
bound
`:suggestions`. Alerts never called `updateStreamKeywords`. PromQL ran a
second
editor runtime purely for its grammar, and asked
`/prometheus/api/v1/series`
— the heaviest endpoint available — for data the stream schema already
has.

## Phases

**1 — icons, staleness, quoting.** Correct `CompletionItemKind` glyphs,
stream
keywords refreshed when the stream changes, `spath` quoting.

**2 — the function catalog.** Custom functions get one tab stop per
argument,
org VRL functions stop appearing twice, column types read from
`dataType` as
well as `type`.

**3 — signature help, hover, async completion.** Providers return real
`CompletionList`s with `incomplete: true`, so a list that arrives late
is
re-queried rather than pinned.

**4 — numeric ranking.** Numeric columns sort first inside a numeric
aggregate.

**5 — values for streams nobody has searched.** The value cache only
ever held
what a search had already returned, so a stream you had not queried
offered
nothing. Values are now fetched on demand, bounded (10s timeout, 60s
cooldown,
one in-flight request per key), and shared between the editor and the
builder.
PromQL joins the same path: labels from the stream schema, values from
the
shared cache. `/series` now has zero production callers.

**6 — one way to fetch a value.** Item 22 only, in this PR.

## The dashboard filter 400

Clicking the ✕ beside "Select Field" in a filter's dropdown sets the
column to
`{}`, and the watcher on `condition.column` then asks for that column's
values.
`loadFilterItem` built `fields: [row.field]` out of `undefined`,
JSON.stringify
turned that into `null`, and the server answered

```
400 {"code":400,"message":"SerdeJsonError# invalid type: null, expected a string"}
```

`_values_stream` answers 400, not an empty result, for each way a piece
of the
payload can go missing — and every one is reachable: an unresolved
stream alias
(`.find(…)?.stream`), a stream not yet chosen (`""`), a range not yet
set. The
range fails differently: `meta.dateTime` starts as `{start_time: ""}`
and
`""?.toISOString()` does not short-circuit, so it throws and both
callers
surface "Something went wrong!" for a lookup the user never asked to
fail.

Guarded at the two callers, where the payload is built and where the
throw
happens. Not at `fetchFieldValues` — a guard there is too late to stop
the
throw and would duplicate validation the callers need anyway. The
trigger is
left alone: re-picking a field must still load its values.

## Notable removals

CodeMirror is gone — it was still bundled for the PromQL grammar alone.
Bundle 26.3 MB → 25.43 MB; the PromQL chunks 442.7 KB → 20.6 KB. The
PromQL
term catalog went from 7 entries to 113, generated from Prometheus's own
tables
and vendored with its MIT notice.

## Verification

- 76 files, +10.5k/−1.4k, ~9,100 tests passing across the touched areas.
- `vue-tsc` clean, `npm run build` exit 0.
- Every phase checked in a running browser against a live instance,
including
the 400 above: the ✕ now produces no request and no toast, adding a
filter and
re-picking a field each still fetch once, and the value list still
populates.

## Known, deliberately out of scope

- A filter still fetches twice (once on add, once on open) — that is
item 23,
  the cache read, not this PR.
- `useMetricsExplorer.loadFilterItem` has the same unguarded
`toISOString()`
  pattern on a different surface.
- `services/metrics.ts` and `services/search.ts` still carry dead
  `get_promql_series` wrappers now that nothing calls `/series`.
2026-08-03 22:29:04 +00:00
Omkar Kesarkhane 021d2135b5 fix(folders): stop the move dialog offering the folder you are already in (#13599)
## The bug

Selecting checks in Synthetic Monitoring and pressing **Move** opened a
dialog reading:

```
Current Folder:  Foo
Select Folder:   Foo
```

The same folder name twice — which reads as "move this to where it
already is".

Nothing could actually go wrong: `primary-button-disabled` already
blocked a same-folder submit. The dialog simply gave no clue *why* Move
was greyed out, because the destination it had chosen for you was the
one destination it would not accept.

## Cause

`MoveAcrossFolders` seeded `selectedFolder` from `activeFolderId`, and
`SelectFolderDropDown` listed every folder including that one.

## Fix

- `SelectFolderDropDown` gains an optional `excludeFolderId` prop: it
filters that folder out of the options and refuses to seed the selection
with it.
- `MoveAcrossFolders` passes `:excludeFolderId="activeFolderId"`, opens
with no destination chosen, and additionally disables Move while the
destination is empty — *"not the active folder"* is not the same as
*"somewhere to move to"*.
- The folder-list watcher now keeps a still-offerable choice. Creating a
folder from the **+** button arrives here as a store list change, and
the re-seed would otherwise clear the folder just created and selected.

## Why the prop is opt-in

`SelectFolderDropDown` has six other consumers — ImportAlert,
AlertList's clone dialog, CreateReport, AddSlo, SloList, and the
synthetics *duplicate* dialog. Every one of those picks a folder to file
something **into**, where opening on the active folder is correct.

Unset, the filter matches every folder (`!== undefined`) and the seeding
is untouched, so those call sites are byte-for-byte unchanged.

## Scope

This dialog is shared, so the change reaches **alerts, pipelines,
reports, SLOs and synthetics**. Behaviour is strictly narrowed: the
option removed was already a guaranteed no-op behind the disabled-submit
guard.

`MoveDashboardToAnotherFolder` uses a separate `SelectFolderDropdown`
under `components/dashboards/` and has the same behaviour, but it is a
different component tree and is deliberately left alone.

## Tests

5 new cases on the dropdown (default offers everything, excluded id
dropped, blank start, normal seeding still works, choice survives a list
change) and 1 on the dialog.

Three existing tests asserted the old seeding and are updated. **One
deserves review:** `should compute primaryButtonDisabled=false when
selectedFolder value is null` asserted that a *null* destination
**enables** Move — executing that submits `dst_folder_id: null`. I read
it as wrong rather than intentional and inverted it. If that null path
is load-bearing somewhere I have not found, this is the thing to
challenge.

## Verification

Run on this branch, based on `main`:

- `src/components/common/sidebar/` — 291 passed (6 files)
- Consumers: SyntheticMonitoring, AlertList, ImportAlert,
MoveDashboardToAnotherFolder — 216 passed, 10 pre-existing skips
- `eslint` on all four changed files — clean
- `npm run lint:design:strict` — OK, no regressions
- `npm run type-check` — exit 0
2026-08-03 11:57:26 +00:00
Omkar Kesarkhane aedcc83a24 fix(synthetics): stop a browser replay without stranding a step in progress (#13592)
## Problem

Recording a browser test and replaying it, then pressing **Stop** in the
journey editor:

1. The step the replay was on stayed stuck in the in-progress (spinner)
state forever.
2. The extension did not stop promptly — it kept going for another step
or two.

## Root cause

One defect produced both symptoms, and it lives in the recorder
extension's player
(`CrxPlayer.stop()` in `openobserve/playwright-crx`): it did not abort
the in-flight
Playwright call. It only raised a flag that `_checkStopped()` reads at
the **start** of an
action — never while a `click` / `fill` / `expect` is pending, and those
carry a 60s timeout.

So after Stop: the current step ran to completion, then the loop
advanced and emitted
`stepStarted` for the *next* step before unwinding. That step could
never report a result.
`useSyntheticsRecorder` set `activeStepId` from it and `stepDotState`
rendered it as
`"active"` — a permanent spinner.

The player fix is a companion PR in `openobserve/playwright-crx`. **This
PR is the O2 half**,
which is also what makes the UI honest while the stop is in flight.

## Changes

- **`ReplayPhase` gains `stopping`** — the interval between the click
and the extension
confirming. Stop becomes a disabled, loading button in the same slot (no
layout shift) and
the banner reads "Stopping replay…", so the UI no longer claims the run
is over while it is
  still winding down.
- **`useSyntheticsRecorder.stopReplay()` owns the `running → stopping →
stopped` transition**,
the way `replay()` already owns `running → passed/failed`. It clears
`activeStepId`, since
the interrupted step never reports a result and nothing else would ever
clear it. Bounded by
the existing `COMMAND_TIMEOUT_MS`, so `stopping` cannot strand the
journey.
- **A replay generation counter** orphans the in-flight replay promise
on stop. Otherwise the
abandoned run answers later and knocks a newer `running` back to
`stopped`, or resurrects a
banner the user just dismissed. Latent before — making stop fast is what
would start firing it.
- **`stepDotState` renders `active` only while `running`**, so an
unresolved step falls through
to `pending` — an empty circle, which is the truth: it never completed.
- **Stream gating** — a late `stepReplayStarted` is ignored outside
`running`; a
`stepReplayResult` already in flight still counts toward "completed X of
N", because that step
  genuinely ran.
- `CreateBrowserTest.onStopReplay` collapses to pure delegation; its
route guard treats
  `stopping` as live.

Also on this branch: a small `cursor-help` → `cursor-pointer` change in
`MonitorTable.vue`
(separate commit, unrelated to the above).

## Deliberately unchanged

The incognito replay window is **not** closed on Stop, so window reuse
across
record → replay → re-run is untouched.
(`crxApp.close({closeWindows:true})` was not an option
anyway — it removes *every* incognito window, including the user's own.)

`stopReplayAndForget` (route guards, `beforeunload`), recording
start/stop, and the
passed/failed replay paths are untouched.

## Testing

Regression tests in `useSyntheticsRecorder.spec.ts`,
`BrowserJourney.spec.ts` and
`CreateBrowserTest.spec.ts` — **each verified to fail with its guard
removed**, including the
headline one: `replayPhase: "stopped"` with a non-null `activeStepId`
must render `pending`,
not `active`.

The end-to-end proof lives in the companion PR: a browser test drives
the real extension in
real Chrome, blocks a step for 40s, presses Stop, and asserts prompt
acknowledgement,
`stopped: true`, and that no later step was announced. It passes in ~5s
with the player fix;
without it the extension never acknowledges the stop within 15s at all.

## Verification

- `npm run lint` — 0 errors in changed files (4 repo-wide errors are
pre-existing in
  `eslint.config.js` and `TraceDAG.spec.ts`)
- `npm run type-check` — clean
- `npx vitest run src/components/synthetics src/views/synthetics
src/composables/useSyntheticsRecorder.spec.ts`
  — **817/817 passing across 44 spec files**

## Note for reviewers

This needs the companion `playwright-crx` player fix to actually stop
promptly; without it the
UI is honest but the extension still winds down slowly. The extension
`dist/` must be rebuilt
and reloaded in `chrome://extensions`.
2026-08-03 11:04:22 +00:00
Loakesh Indiran f16e8e53ff perf(synthetics): config caching, run-budget validation, HA correctness, step-grain Steps tab (#13580)
Synthetics performance and correctness work. Four areas: caching the
config reads on the job path, a false-alarm bug in validation, four HA
correctness bugs found by auditing what happens with more than one alert
manager, and the Steps tab moving to a step-grain stream.

Pairs with o2-enterprise#2325 — **merge this first**, that one calls
functions introduced here.

---

## Caching
- [x] Probe-token auth cache — 10s TTL, negative results cached so a bad
token can't force a query per request
- [x] Locations whole-table cache — 30s, serves `get` / `find_by_pool` /
`list_visible` from one load
- [x] Check definition cache via `get_cached()` — was read twice per job
(resolve + ack)
- [x] Agent capabilities cache via `get_cached()` — was read on every
lease poll
- [x] Org ingest-token cache — `find_default_enabled()` replaces three
hand-rolled `list_by_org().find(enabled)` copies
- [x] `list_by_locations()` — one batched agents query, replacing one
per location

## Cross-node invalidation
- [x] `coordinator::synthetics` — event invalidation following the
alerts/pipelines pattern
- [x] Write paths call `invalidate_and_publish()`, the watcher calls
plain `invalidate_cache()` so events don't echo
- [x] Deletes emit a delete event, so coordinator keys don't accumulate
one per check ever created
- [x] Ingest-token invalidation reuses the existing
`/org_ingestion_tokens/` watch — no second event stream
- [x] SQLite write mutex released before emitting — `get_lock()` and the
coordinator's `put()` take the same lock, so emitting while holding it
deadlocked the process

## HA — correctness with more than one alert manager
Confirmed with @subhradeep that a region can run multiple alert
managers. The synthetics scheduler, dispatcher, reaper and staleness
watcher all run on **every** one of them.

- [x] **Duplicate runs** — `claim_due()` claims due checks with `SELECT
… FOR UPDATE SKIP LOCKED`, advancing the schedule inside the same
transaction
- [x] **Duplicate "location down" pages** — the one-shot flag moved from
an in-process `HashSet` to `synthetics_locations.down_notified_at`,
claimed with a CAS
- [x] **Duplicate run completion** — `increment_jobs_done` claims
completion via `completed_at IS NULL`; two acks of the same run could
both be told they completed it
- [x] **Inert dedup index** — `scheduled_ts` is now the schedule slot,
not each node's tick clock, so the existing `synthetics_jobs_dedup_uq` +
`ON CONFLICT DO NOTHING` actually fires
- [x] Migration `m20260803_000001` adds `down_notified_at`, defaulted so
no backfill is needed
- [x] `DB_SCHEMA_VERSION` 62 → 63 — without it the migration never runs
on an already-upgraded deployment

## The false-alarm fix
- [x] Validate against the run budget (840s), not the job lease (900s)
- [x] The three bounds are deployment-configurable:
`O2_SYNTHETICS_MAX_CHECK_BUDGET_SECS` / `_JOB_LEASE_SECS` /
`_MAX_NET_TIMEOUT_MS`
- [x] Rejected limits are logged, not fatal — nothing installed means
validation keeps the conservative defaults
- [x] Browser retries capped at 2; protocol types stay at 3; default
stays 0 for every type

## Steps tab — the step-grain stream (B10)
- [x] Three `GROUP BY`s over the step stream replace downloading 5000
execution rows and tallying their JSON in the browser
- [x] Falls back to the old tally when the stream is empty, so the tab
does not blank for checks whose agents have not been upgraded
- [x] Gated on the `step_id` column rather than stream existence — a
partially-created stream would otherwise cause four failed queries

## Overview KPIs
- [x] Count tiles summed from the cached histogram; `buildKpiSql`
deleted

## Rename
- [x] `synthetics_monitors` → `synthetics_checks`, `monitor_type` →
`check_type`
- [x] Three UI call sites that read the old field names, including a
delete guard that silently disabled itself

---

**Why validation moved off the lease.** Three bounds stack:

```
check worst case  <=  max_check_budget_secs  <  job_lease_secs
     840s                    840s                   900s
```

Validation used the lease, so the server accepted checks the execution
environment cannot finish. On the managed path the probe is a Lambda,
and a function that hits its timeout is killed mid-run — lease headroom
is irrelevant once the process is gone. The net probe's function timeout
had drifted to **60s** while validation accepted up to 900s, so a plain
`timeout_ms=30s, retries=2` (worst case 100s) was accepted and then
killed, reporting a failure the target never had.

**Why the limits are ENT-declared but OSS-consumed.** Synthetics is
enterprise-only, so the values live in `SyntheticsConfig`. The
validation they bound lives in `config`, which cannot depend on
`o2_enterprise`. `SyntheticsLimits` + `init_limits` is the seam, falling
back to built-in defaults in OSS builds and tests. A bad limit is logged
rather than fatal: refusing to start ingest, search and dashboards
because a probe ceiling is misconfigured would be the worse outage.

**The duplicate-run bug.** `fetch_due` was a plain `SELECT` and the
advance was an unconditional `UPDATE … WHERE id = ?`, so nothing stopped
two alert managers firing the same check. Each inserted its own
`synthetics_runs` row under a fresh KSUID, which made the resulting job
rows genuinely distinct — `lease_batch`'s `status = 0` guard cannot
collapse them, because they are not contending for one row. N alert
managers meant every check ran N times.

`SKIP LOCKED` rather than a compare-and-swap because it *distributes*: a
second scheduler skips the rows the first holds and picks up different
due checks, so N replicas split the backlog. A CAS gives the same
exactly-once guarantee but every node reads the same candidates, one
wins them all, and the losers each issue a doomed `UPDATE`. The lock and
the advance share a transaction because the locks only live until
`COMMIT`; the fan-out stays outside it so config rows are not locked
across N job inserts.

**Duplicate run completion needed no second alert manager.** The
increment was atomic, but the completion decision re-read the row in a
second statement and returned "complete" to whichever caller saw
`jobs_done >= job_count` — which both acks of a 2-job run do. Acks are
served on ingesters, of which every environment runs two, and the probes
of one run finish independently. Effect: `consecutive_failures` counted
one run twice on every check, plus a genuinely duplicated notification
on checks left at the default `cooldown_mins = 0`.

**The Steps tab was truncating silently.** It downloaded up to 5000
execution rows and tallied `last_attempt_steps` in the browser. On the
busiest check measured — 18,079 executions over 7 days — that cap
covered 4 of the 7 days and reported a **56.3% fail rate against a true
33.7%**. The win is correctness; the ~18MB payload reduction is
secondary.

## Verification
`cargo check --workspace`, clippy, fmt clean. Config 98, infra 33
synthetics tests pass. Frontend: **839 synthetics tests pass**. Three
frontend spec files fail to import on `useLocalTimezone` — verified
identical on clean `main`, unrelated to this branch.
2026-08-03 10:14:11 +00:00
4fa71dd15c feat(ui): calm-signal pass over IAM and Synthetics Monitors (#13568)
Second batch of the "Calm Signal" pass (after #13562: Streams,
Pipelines, Nodes).

## Users
- Summary strip that doubles as the role facet. Tiles are the roles
**actually present in the data**, so custom roles appear without being
enumerated in code. Counts are role *membership* — a user with Admin + a
custom role is counted under both, so tiles intentionally don't sum to
the total. Top 5 by size; the tail folds into one "+N more roles" tile
counting unique users.
- Pending users now show the invited chip on the enterprise path (it
only appeared on the open-source one).
- Custom roles get one stable neutral chip instead of an untyped tag
whose colour was derived from the role's spelling.
- Removed a `<style scoped>` block whose only selector is used nowhere.

## Roles
- Member count per role, muted when nobody holds it. Comes from the
batched user→roles map — **one request for the org, not one per role** —
and is gated to enterprise, where that endpoint exists. Rows render
immediately; counts patch in when they land.

## Service Accounts
- Created reads as relative age, with a dot on keys minted this week.

## Synthetics Monitors
- Status counts were hidden inside a dropdown's option labels ("Passed
(12)"). They're now a strip that acts as the facet; the dropdown and its
dead options plumbing are gone.
- Health row rail, plus the red / muted row wash for failing and paused
monitors that Alerts and Pipelines already use.
- Last check renders through `OTimeCell` instead of a local time-ago
copy.
- **Fixes styling that never applied:** the uptime sparkline and its
hover tooltip referenced `.spark` / `.stt-*` classes that are defined
nowhere in the repo, so the bars had no size and the tooltip no surface.
Rebuilt with tokens.
- `timezone` is a prop now — a leaf table shouldn't reach into the
store.

## No strip on Dashboards, Roles, Service Accounts or Groups
Their candidate tiles were structurally constant (System = 1),
near-always zero, derivable from a neighbour, or already in the table
footer. Dashboards was already compliant (favourites, owner cell,
relative dates) and is untouched; Groups' API returns names only, so
per-row counts would cost one request per group.

`calm-signal.md` records both rules this batch produced: the
three-question test for a tile, and rail-vs-wash (a full-row wash only
when the state means *act now*, is rare, and the row is the unit of
action).

## Testing
`lint`, `lint:design:strict`, `lint:styles`, `lint:tokens`,
`lint:token-purity`, `type-check` pass. 885 unit tests across IAM and
Synthetics pass.

Synthetics needs a monitor in each state to eyeball the rail/wash;
Roles' member column needs an enterprise build (403s and hides itself
otherwise).

---------

Co-authored-by: ktx-vaidehi <134508096+ktx-vaidehi@users.noreply.github.com>
Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech>
2026-08-03 08:54:15 +00:00
ktx-vaidehi f8253d4b24 fix: dashboard table chart width (#13587) 2026-08-03 07:55:33 +00:00
7f87242ac9 fix: dashboard reports drawer duplicates entries and hides reports in custom folders (#13569)
## Summary

This PR fixes two bugs in the Dashboard Reports drawer
(`ScheduledDashboards.vue` / `ViewDashboard.vue`).

**Issue 1 — Reports duplicate on every reopen**
Opening the Reports drawer, creating a report, then reopening the drawer
for the same dashboard caused the same report to appear as a duplicate
row. Reopening again added yet another duplicate, so the count grew by
one on every open. This happened because the drawer component never
unmounts between opens (it's toggled via `v-model:open`, not `v-if`),
and the report list was being appended to on each reopen instead of
being rebuilt.

**Issue 2 — Reports in a custom folder don't show up**
Reports created from a dashboard using the Default folder appeared
correctly in the drawer, but reports created using a custom report
folder did not — even though creation succeeded. The drawer's list
request was filtering by the dashboard's own folder id, but that filter
is meant to match the *report's* folder, not the dashboard's. Any report
saved to a folder other than the dashboard's own folder was silently
excluded.

| # | Issue | Root Cause | Fix |
|---|-------|------------|-----|
| 1 | Reports duplicate in the drawer each time it's reopened |
`formatReports()` in `ScheduledDashboards.vue` pushed new rows onto the
local `scheduledReports` list without clearing it first; since the
drawer stays mounted across opens, old rows persisted and new ones piled
on top | Rebuild the list fresh from `props.reports` on every call
instead of accumulating |
| 2 | Reports saved to a custom folder don't appear in the dashboard's
Reports drawer | `openScheduledReports()` in `ViewDashboard.vue` passed
the dashboard's folder id as the `folder_id` filter on the reports list
API, which filters by the *report's own* folder, not the dashboard's |
Removed the folder filter so the drawer lists all reports for the
dashboard regardless of which report folder they're saved in |

Issue : https://github.com/openobserve/o2-enterprise/issues/2319

---------

Co-authored-by: ktx-vaidehi <134508096+ktx-vaidehi@users.noreply.github.com>
Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech>
2026-08-03 07:28:17 +00:00
Shrinath Rao de95f7f2d8 fix: restore Table default log-detail view (#13368) + heal alert-name inline-edit read (#13585)
## What & why

Two **Playwright Regression** nightly failures, reproduced identically
on both OSS ([run
30785096054](https://github.com/openobserve/openobserve/actions/runs/30785096054))
and ENT ([run
30785071382](https://github.com/openobserve/o2-enterprise/actions/runs/30785071382))
— same two shared-`tests/` specs, all 3 retries failed each. Both are
OSS-owned specs (ENT overlays only its own `tests/*` and builds OSS
`web/`), so this single OSS PR fixes both nightlies.

### 1. Logs — real product regression (`logs-regression.spec.js:1158`)
`Log detail sidebar opens with Table tab by default (#13368)` → `Error:
Table tab should be selected by default`.

**Root cause:**
[#13368](https://github.com/openobserve/openobserve/pull/13368) (Jul 23)
deliberately made the log-detail sidebar open on the **Table** tab.
[#13451](https://github.com/openobserve/openobserve/pull/13451) (Jul 30,
*"table migration to same components"*) silently flipped
`detailTableInitialTab` back to `"json"` in `SearchResult.vue` (both the
`ref` init and the per-open reset). The unchanged test correctly caught
the reverted behavior.

**Fix:** restore `"table"` as the default log-detail tab. Product fix —
no test change.

### 2. Alerts — stale test vs redesigned component
(`alerts-stream-switching.spec.js:443`)
`Bug #11577: Alert preview chart y-axis displays numeric values` →
`TimeoutError: locator.inputValue: Timeout 45000ms exceeded`.

**Root cause:** the alert name became an **`OInlineEdit`** title
(`OFormInlineEdit`, from the
[#13547](https://github.com/openobserve/openobserve/pull/13547) Alerts
revamp). In display mode the value lives in the `…-value` span; the
`…-input` only exists **while editing**. The test read `.inputValue()`
on the non-existent input → 45s timeout.

**Fix:** read the committed display value via a new `getAlertName()`
page-object helper (with an edit-mode `inputValue()` fallback).

## Verification
- **Alerts #11577** — ✅ passed locally against a local O2 build (`1
passed`); `getAlertName()` reads the name and the full
save/verify/delete flow works.
- **Logs #13368** — restores the exact `detailTableInitialTab = "table"`
value #13368 set and the unchanged test already validated as green.
Confirmed by this run's Logs-Regression job + the dispatched ENT e2e
gate.

## Scope
- `web/src/plugins/logs/SearchResult.vue` — 2 lines (product)
-
`tests/ui-testing/playwright-tests/RegressionSet/Alerts/alerts-stream-switching.spec.js`
— read display value
- `tests/ui-testing/pages/alertsPages/alertsPage.js` — `alertNameValue`
locator + `getAlertName()` helper
2026-08-03 07:19:28 +00:00
Omkar Kesarkhane fc8e3c1657 fix: redesign the step editor and the run evidence surfaces (#13582) 2026-08-03 12:45:16 +08:00
Ashish Kolhe 23e816b1e4 feat: External Alert Sources incident ingestion (#13578)
## Summary
Adds a full "External Alert Sources" feature so OpenObserve Incidents
can serve as ingestion/correlation layer for third-party alert tools
(Grafana, Alertmanager, generic webhooks):

- Token-authenticated inbound webhook endpoint (`o2iat_` tokens) that
normalizes Grafana/Alertmanager/generic payloads and correlates them
into Incidents alongside internally-generated alerts.
- Incident integrations CRUD (create, list, enable/disable, rotate
token, delete — default integration protected from deletion) with
per-sender observed status.
- Settings UI: single always-open alert sources table (merged from an
earlier two-panel design), shared-token badge, add/rotate/delete flows.
- Incident detail UX: contributing-alert Source/Labels columns, "view
raw payload" action (surfaces the exact webhook JSON received), and an
external-source badge on the incident header so it's obvious at a glance
whether an incident has external contributors.
- **Auto-resolve**: incidents now close automatically once every
contributing external alert reports `status: resolved` — previously
required manual resolution even when the source alert cleared.

## Notable follow-up fixes bundled in
- Rebased onto latest `main`; repaired two build breaks surfaced by
upstream changes that landed after this branch diverged
(`CorrelationSubject` refactor's new `eval_level` param, and a new
`SeverityEscalated` correlation outcome) — both textually merged clean
but were semantically incomplete for the external-event path.
- Removed the `O2_INCIDENTS_EXTERNAL_SOURCES_ENABLED` enterprise config
flag (companion removal in o2-enterprise) — external sources are now
gated solely by `O2_INCIDENTS_ENABLED`, removing a redundant toggle.

## Test plan
- [x] `cargo build --no-default-features --features enterprise` — clean,
no warnings introduced
- [x] `cargo clippy` on touched crates (`infra`, `openobserve-core`,
`openobserve-api-management`, `openobserve-api-http`) — no new warnings
- [x] `cargo fmt --check` — clean on all touched files
- [x] `cargo test` — all tests in touched modules pass
(`alert_incidents`, `incident_integrations`, `external_alerts`,
`alerts::*` — 274/274); two unrelated flaky DB-pool-timeout tests
(`db::tests::test_delete`,
`table::ratelimit::tests::test_add_batch_missing_rule_id`) confirmed
pre-existing and pass in isolation
- [x] Frontend: `vitest run` on all touched components — 156/156
passing; `vue-tsc --noEmit` clean
- [x] Manually verified end-to-end against a running dev server:
fired/resolved external alerts, confirmed auto-resolve, delete, and
raw-payload retrieval all work against real HTTP calls
2026-08-02 16:16:50 +00:00
Prabhat Sharma 0cfa518433 feat(slos): SLO measurement UI — trend charts, previews, and slice defaults (#13579)
## What this is

The SLO measurement UI: the Reliability nav entry and SLO routes, the
list and
detail pages, the create/edit form, and the trend charts that make an
error
budget readable.

## The charts

The detail page draws **burndown** and **burn rate** side by side. They
read the
same slices and differ only in the window they aggregate over, which is
what
makes the pair legible: burn spikes above ×1, budget bends downward.

Each panel carries a **second y-axis** that relabels the first through a
fixed
affine map — a burn multiple *is* an SLI, a share of budget *is* a
number of
errors. This is not the dual axis that makes crossing points an
artifact: there
is one series, and the right axis is the same axis in the unit people
speak in.
The SLO is written as "99.9%", and "we can afford 12 more errors" is a
sentence
an on-call engineer can act on.

That demands exact gridline alignment, so `chartScale.ts` pins the left
axis and
*derives* the right one. Two consequences worth knowing:

- **Precision follows the axis step, not the unit.** A four-nines target
leaves a
0.01-wide budget, so its SLI axis moves in thousandths — two decimals
would
  print every gridline as "100.00%".
- **The burndown's top is pinned at 100**, not rounded up to a multiple
of its
step. A window 1,500% overspent takes a 500-point step, and rounding the
top
squashed the whole healthy stretch of the line into the top fifth of the
panel.

An info tooltip on each panel gives the plain reading, the formula, and
how to
read the threshold line. The formulas mirror `toBurndownSeries` (and so
`config::meta::slo::math`) exactly — two statements of "burn rate" that
disagree
by a rounding convention is a support ticket.

## Previews in the form

A count SLI is a **predicate** and the doubt is whether it parses; a
time-slice
SLI is a **number** someone has to guess (232 ms) and nothing told them
whether
that makes 1% or 40% of slices bad. Each now gets the preview that
answers its
own question, and the time-slice one puts the verdict in the header —
"83.3%
good — 5/6 slices" — beside the target just typed.

The threshold never reaches SQL, mirroring the ingest pass, which
aggregates
first and classifies after so a threshold edit re-reads stored slices
instead of
contradicting them. Dragging it therefore rescores instantly with no
query.

The aggregate field moves from a bare input to the same expression
editor scope
and good-when already use — it names a column, and a mistyped column is
invisible until the ingest query fails.

## Defaults

**5-minute slices are now the form default.** For a count SLI the slice
width does
not touch the arithmetic — the window SLI is Σgood/Σtotal and
repartitioning the
same events changes neither sum — so 60s returned bit-identical numbers
for five
times the stored rows (139,680 per series at the 97-day horizon against
27,936).
60s stays available because for a time-slice SLI the width *is* the
definition:
at 99.9% over 7 days a single bad 5-minute slice spends about half the
budget.
Backend validation is untouched; both widths remain legal.

## Verification

Beyond unit tests, the charts were rendered against a live instance,
which caught
three defects the tests could not:

- The burndown's second axis counted **seconds** for time-slice SLOs
while
labelling them slices — off by 300× (2,340 shown where the honest answer
is
  7.5). Now converted via a tested `budgetUnitsFor`.
- `histogram()` returns `slice_start` as an ISO string without a zone
marker.
`Number()` gave NaN (empty panel forever); parsing as local time would
have
  shifted the series by the viewer's offset.
- The preview's result set is not the range — a slot with no rows
produces no row
at all, so the tally read over an undisclosed sample (4 of 12 slots on a
7.3%-coverage SLO). The gap count is now stated, and the pass/fail
colour is
withheld below ten measured slices rather than calling 66.7% a failure
on the
  strength of three.

`npm run lint`, `vue-tsc`, `lint:design:strict` and the `utils/slos`
suites all pass.

## Known follow-up, deliberately not in here

SQL function autocomplete renders its whole call as the label, uses the
"abc"
(Text) icon, and inserts a literal `'undefined'` for any function taking
more than
one argument — the provider passes one argument to a label builder that
takes
several. It is app-wide (`useSuggestions.ts` + `CodeQueryEditor.vue`),
not
SLO-local, so it wants its own change.
2026-08-02 16:06:05 +00:00
Prabhat Sharma d99ecff3e8 feat(nav): add Reliability menu, restore deferred SLO routes (#13577)
Groups the alerting surface under one left-rail tile, and un-defers the
SLO backend that shipped disabled.

> **Lands with** openobserve/o2-enterprise#2321, which adds
`ROUTE_PERMISSIONS` for the restored SLO endpoints — the route-coverage
test pairs the two, so this one alone fails it.

## Reliability menu

New `reliability` nav group absorbing **Alerts, SLOs, Incidents**, plus
**Notification Destinations** and **Templates**, which move out of
Settings — they are alerting configuration, not deployment
configuration. Settings keeps Pipeline Destinations (that one really is
pipeline config); its group is renamed "Destinations".

Their routes are **top-level and flat** (`/alert-destinations`,
`/alert-templates`) rather than nested under `/alerts`. They are
siblings of Alerts, not sub-pages of it — nesting them made the URL
claim otherwise and the rail believed it, highlighting Alerts alongside
them.

- Route **names** are unchanged, so the ~15 call sites that navigate by
name are untouched.
- Old `/settings/*` paths redirect, with the query preserved —
`?action=import` deep links keep working.

## SLO routes

`/api/{org}/slos` answered **404**. The routes were deliberately
unregistered before the last release (`6977b4020d`, `ac78a622e1`) while
the rest of the feature — `core/src/slo/*`, the infra tables, both
migrations, the `slo_maintenance` job, all seven handlers — stayed in
the tree. This restores the five registrations and the OpenAPI entries,
recovered from the pre-strip commit rather than rewritten.

`ZO_SLO_ENABLED` still **defaults to false**. It is now published as
`slo_enabled` on `/config`, so the menu entry follows the flag instead
of offering a page the API answers with 501.

## Nav active-state

Active state was decided per child, so any section whose path prefixed
the current route lit up alongside it. It is now resolved once per
flyout: exact route-name match wins, else the **deepest** path prefix —
so a genuine drill-down like `/alerts/detail/:id` is still attributed to
Alerts. `placeAfter` also accepts a group key, letting Data anchor on
the Reliability tile it follows rather than on an item that tile
absorbs.

## Tests

`tests/ui-testing` referenced `[data-test="alert-destinations-tab"]` /
`alert-templates-tab` in six files — those data-tests were on the
Settings rail items removed here, so the locators would have matched
nothing and hung until timeout. Those paths now go through the existing
`openNavFlyoutChild` helper, extended with the Reliability group. Two
URL regexes that would have silently mismatched on the hyphen are fixed
(one ended in `.catch(() => {})`, so it would have degraded to a silent
15s stall).

## Verification

- 3597 unit tests pass; `vue-tsc` and eslint clean; enterprise build
compiles.
- `playwright test --list`: all 141 tests across 23 files load, no
module errors.
- Against a live server: `GET /slos` → 200, `POST /slos/move` → 422 (so
the literal wins over the `{slo_id}` catch-all), `GET /slos/x` →
handler-level `{"code":404,"message":"SLO not found"}`.
- In the running app: `/alert-destinations`, `/alert-templates` and
`/alerts` each highlight exactly one row;
`/settings/alert_destinations?...&action=import` redirects with the
query intact; rail order unchanged.
2026-08-02 05:44:01 +00:00
Hengfei Yang 72f7bff378 perf: optimize AI session list queries (#13574)
## Summary

- Page sessions by `session_id` before computing the expensive summary
fields.
- Aggregate only the selected page of sessions with exact
`count(DISTINCT trace_id)`, removing trace ID materialization and
Rust-side per-trace rollups.
- Generate direct Agent predicates and evaluate them as aggregate
membership conditions without a session-membership subquery.
- Support efficient server pagination by fetching one extra session to
determine `has_more`, without counting every distinct session on every
page.

## Why

The previous endpoint first materialized `array_agg(DISTINCT trace_id)`
for every candidate session and then issued another query using those
trace IDs. A single-pass full rollup was also slower because it
aggregated every summary field for all matching sessions before applying
TopK.

This change keeps the inexpensive session page selection separate and
bounds the expensive rollup to the requested page size. Page selection
and display both use the latest `end_time` across the complete session;
Agent predicates only decide whether a session is a member of the result
set.

The endpoint previously returned `total = hits.len()`. With a 20-row
page, OTable therefore calculated one total page and permanently
disabled Next even when more sessions existed. An exact `count(DISTINCT
session_id)` query scanned the full data set and took about 1.18 seconds
by itself on this benchmark. Instead, the page query now requests `size
+ 1`: while another page exists, `total` is a documented lower bound and
the response includes `has_more=true` / `total_is_exact=false`. The
final page returns the exact total. OTable displays lower bounds with
`+` and hides Last-page navigation until the exact total is known.

## Performance

Measured locally against `bench_traces` (167 files, about 99.9 million
records), using five runs and ignoring the first two:

| Query shape | Mean wall time |
| --- | ---: |
| Original trace-ID flow | 6.829 s |
| Page session IDs + membership subquery | 3.219 s |
| Page session IDs + Last activity | 0.850 s |

The optimized query shape is about 87.6% faster than the original flow.
The second-phase rollup returned 20 rows with about 74 KB of
intermediate output and about 2.45 MB peak memory in the query plan.
Fetching the extra pagination sentinel did not introduce a meaningful
page-size-dependent regression in the final runtime sweep.

## Behavior note

The Agent filter controls membership through `HAVING max(CASE WHEN
<filter> THEN 1 ELSE 0 END) = 1`, while the final rollup includes all
spans for each selected session. Both pagination and display use the
complete session's `max(end_time)`, ordered by `session_last_activity
DESC, session_id DESC` for stable offset pagination. The UI labels this
value `Last activity`; session start and duration remain available from
the rollup.

Current clients send direct Agent predicates. For compatibility, the
endpoint also recognizes the previous narrow same-stream `session_id IN
(SELECT session_id ... GROUP BY session_id)` shape and safely extracts
its inner WHERE predicate; unrelated subqueries are left unchanged.

This assumes every relevant span has a `session_id`.

## Validation

- `cargo build --features mimalloc --profile release`
- `cargo test -p openobserve-api-search traces::session --lib` (18
passed)
- `cargo test -p openobserve-api-search user::tests --lib` (13 passed in
the earlier query-change validation)
- `cargo fmt --all`
- `git diff --check`
- targeted Vitest: Sessions composable, SessionsList, and OTable (169
passed)
- `vue-tsc --noEmit -p tsconfig.vitest.json --composite false`
- targeted ESLint and Prettier checks
- runtime pagination check against the supplied legacy-filter curl:
  - page 1: 20 hits, `total=21`, `has_more=true`, `total_is_exact=false`
  - page 2: 20 hits, `total=41`, `has_more=true`, `total_is_exact=false`
- final page: 3 hits, `total=184443`, `has_more=false`,
`total_is_exact=true`
- pages 1 and 2 returned 40 unique sessions in descending `end_time`
order across the page boundary
2026-08-01 08:35:55 +00:00
Prabhat Sharma 8f60dbcc82 fix(metrics): widen rate windows when the scrape interval overstates cadence (#13576)
## Problem

Metrics explorer cards for histograms, counters, and summaries showed
**"Too few samples for a rate"** even though the streams had data. The
rate window is sized from the org's configured `scrape_interval` (e.g.
15s → `[1m]`), but when data actually arrives less often (the OTel demo
app exports every 60s), `rate()` almost never finds the two samples it
needs — so every rate-based card rendered the sparse message while
gauges charted fine.

Reproduced against a live instance:
`sum(rate(app_cart_add_item_latency_count[1m]))` → empty; the same query
with `[4m]` → `0.172/s`.

## Fix

**Widened-window retry** (`useMetricsExplorerGrid.ts`,
`computeWidenedRateWindows` in `metricDefaults.ts`): when a rate-based
card comes back empty but the presence probe confirms samples exist in
the window, retry with geometrically wider windows — 4×, then 16× the
standard window, capped at half the range — and chart the first rung
that answers. The sparse message now only appears when no honest window
could produce a chart.

**Editor / dashboard handoff** (`MetricsExplorer.vue`): the drill-in
normally hands the editor `$__rate_interval`, which resolves from the
same overstated scrape interval — opening a blank editor for a metric
the card visibly charts. The preview now remembers which rung worked
(`widenedRateWindow`, persisted through the card cache like
`nanGuardApplied`), and both the card→editor drill-in and the
favorites→dashboard conversion hand over that concrete window instead.
Cards that never needed widening keep the adaptive `$__rate_interval`
behavior.

Supporting changes:
- `previewKeysOf` enumerates the widened query keys so refresh
invalidates them instead of replaying stale retries from the queue
cache.
- Persisted-cache identity gains a shape version (`v: 2`) so pre-field
entries (widened data with no memory of the window) miss once and
re-learn live.
- A failing widened retry settles for the sparse answer already in hand
rather than repainting the card as an error; cancellations still abort.
- During a refresh the loading state carries `widenedRateWindow`
alongside the old results it keeps on screen, so a mid-refresh drill-in
stays correct.

## Verification

- Unit tests: retry ladder math (caps, dedup, no-room-to-widen), first-
and second-rung success, all-rungs-fail keeps the sparse card, window
carried on the preview, handoff passes the concrete window vs
`$__rate_interval` — 182 tests across the four spec files pass; ESLint
and vue-tsc clean.
- Live against a dev backend: previously-sparse `app_cart_*` cards all
render (network log shows `[1m]` → empty, presence probe, `[4m]` →
data); drill-in opens the editor seeded with `sum(rate(...[4m]))` and a
rendered chart, from both the live-query and cache-restored paths.
2026-07-31 19:45:26 +00:00
ktx-vaidehi 77d62ddee2 fix(logs): bound the last selected column to the viewport (#13573)
`body` — and any last selected field — sized itself to its longest value
instead of taking the leftover width, dragging the row into thousands of
pixels of horizontal scroll. Two separate causes:

- the absorber marked the column `meta.autoWidth`, which in OTable means
"no width at all", and a horizontalScroll table carries `min-w-max`,
which sizes every column to its max-content;
- sized columns in a scrolling table had no `max-width`, so as soon as
another field was added and `body` stopped being the absorber, its own
text stretched it to ~3000px.

Split the two behaviours the pre-migration table kept separate: plain
`autoWidth` stays content-sized (the default `source` column was `width:
auto` and scrolled), while the new `meta.fillRemaining` takes the
leftover width but stays inside the container and ellipsis-truncates
(pre-migration `flex: 1 1 auto; overflow: hidden`). A bounded filler
drops the table's `min-w-max` and pins its sized siblings at their own
size in both directions.

Opt-in, so the other horizontalScroll tables (traces, dashboard panels,
monitors, search history) keep their existing layout untouched.

Verified against the pre-migration build (044f561506) running side by
side on identical data: column widths and scroll offsets match exactly.
2026-07-31 14:12:12 +00:00
Omkar Kesarkhane bbed40d8f4 feat(synthetics): author-owned locators, and step-scoped run evidence (#13563)
## Summary

Three synthetics workstreams on one branch: author-owned locators (Phase
2b), retirement of version-1 monitors (Phase 2c), and making a failed
browser step diagnosable without leaving the Steps tab.

Everything else in the synthetics suite passes (422 tests across the 21
suites the evidence work touches).

## Author-owned locators (Phase 2b)

The recorder proposes a locator bundle; the author owns its order.
Schema, combined-locator build/read, provenance pinned across the
recorder bridge, and the payload contract tested from both sides.

`337911158d` `e63f8f429f` `50b811a865` `2a978b9408` `c8c081c3c5`
`35ef1c36cb`

## Version-1 retirement (Phase 2c)

Stop creating v1 monitors, delete the v1 migration path and its step
fields, drop `steps_version` from the schema.

`b58b03077d` `75984ad222` `68a33e885d` `a2bffae7b7`

## Run detail — evidence, scoped to the step

Implements §5.3 of `docs/synthetics/step-failure-evidence-design.md`
("render the bundle, scoped to the step"), which was specified and never
built: only the kind-grouped run-level Evidence tab shipped.

**Per-step page activity.** The expanded step now shows the
`evidence.ndjson` events attributed to it — severity-ranked, capped at
5, with "View all N →" deep-linking into the Evidence tab pre-filtered
to that step. One shared `OTable` wrapper renders rows for both surfaces
so they cannot drift, and one composable fetches the bundle once for
both.

`a483796d57` `a7cbe0a3e0` `f63973285e` `eb7ec4f3e6` `1ed226febe`
`e3d969bcb2`

**Settle signals were attributed to the wrong step.**
`failure_detail.settle_signals` accumulates across the whole journey. On
live run `3HFCZ3fm` it held 13 signals — one from step 2, six from step
8, two from step 14 — while the failed step 15 owned none of them, and
the panel rendered all 13 under step 15. The signal that mattered
(`stale POST **/_search_stream, 30274ms`) belongs to step 14, which the
run's own error string says, and step 14 displayed nothing.
`last_attempt_steps[i].settle_signals` had carried correct per-step
attribution all along and was read by nothing.

`6ba6f8d633`

**Artifact URLs carried no origin.** `artifactUrl()` returned a bare
path while every other call in that service goes through `http()` with
`baseURL: store.state.API_ENDPOINT`. Artifact URLs go to `<img src>` and
`fetch()` directly, so in dev they resolved against Vite on `:8081`,
which has no `/api` route and no proxy — every screenshot and evidence
bundle 404'd. Verified against a running backend with a real key:
`:8081` → 404, `:5080` → 200. Production, served from the API's own
origin, is unchanged.

`baa7407cb5`

**The locator ladder was invisible.** `candidates_tried` lists only the
rungs the probe stood on, so a step that matched on its primary looked
identical to a step that had no second candidate — both one row, both a
grey `used_as_primary` badge. On live run `3HGDMrlU` the failing assert
had one authored locator and every `assert`/`fill` step in that journey
is built the same way, while `click` steps average 2.4 candidates. The
section now leads with "N of M tried", names a one-rung ladder as having
no fallback, labels outcomes in words instead of enum values, and dims
unreached rungs the way the timeline dims a skipped step.

`c05e1ce133`

## Test plan

- `npx vitest run src/views/synthetics src/components/synthetics/results
src/components/synthetics/StepEvidence.spec.ts
src/composables/synthetics src/composables/useSyntheticEvidence.spec.ts
src/services/synthetics.spec.ts` → 422 passing
- `npm run type-check` → clean
- `npm run lint:design:strict` → 2 pre-existing regressions above (see
Known failing)
- Behaviour on the run-detail changes was verified against live records
from `OpenObserve Sys Query` and `OpenObserve Cloud Happy Path` rather
than fixtures alone

## Not covered

- The "Add a fallback locator" action from the locator-ladder work needs
a step-level deep link; `synthetics/edit/:id` has no step anchor, so the
warning states the finding without an action.
- Browser-level confirmation of the artifact-URL and locator-ladder
changes is outstanding — the local backend's search service was
returning `tcp connect error` during verification, so those two were
confirmed against records and by unit test rather than in a running
page.
2026-07-31 11:19:46 +00:00
073c5c2680 feat(ui): calm-signal pass over Streams, Pipelines and Cluster Nodes (#13562)
Continues the "Calm Signal" colour language shipped on the Alerts list:
a calm neutral canvas, with saturated colour spent only on the one
signal each screen exists to surface. These three pages were picked from
a ranked audit of the remaining list screens.

## Streams — `web/src/views/LogStream.vue`

Signal: **ingestion liveness**.

- **Last Ingested** column — relative time from `stats.doc_time_max`,
with a dot on streams taking data within the hour.
- **Liveness row rail** — green (ingested today) / amber (was ingesting,
quiet for over a day) / grey (never ingested). Grey rather than amber
for "never": a brand-new or schema-only stream is legitimately empty, so
amber there would cry wolf on every fresh stream. No full-row wash — a
stream list has no outright failure state, and washing "quiet" or
"empty" would tint most rows in a normal org.
- **Compression** column from ingested/compressed size, coloured only
when it drops below 1x (the compressed copy is *larger* than the raw
data — compression is doing nothing for that stream).
- **Org footprint strip** in `OPageLayout`'s `#subnav`, deliberately
**outside** the table: the list is server-paginated and these totals
cover every stream type, so in the table's `#subheader` they would read
as a summary of the visible rows. Same figures, labels, icons and
formatters as the Home → Usage tiles.

**Bug fixed along the way:** `compressed_size` / `index_size` were
declared outside the row `map`, so a stream without `stats` inherited
the previous row's numbers. Rows now carry raw per-row values and render
`—` when a stat is absent, instead of a misleading `0 MB`.

## Pipelines — `web/src/components/pipeline/PipelinesList.vue`

Signal: **operational state**.

- **State** column (Errored / Paused / Active) built the same way as the
Alerts reference, plus a selectable state strip and a matching row rail.
Errored rows also show when they last failed — recency on the exception
only.
- The state facet is orthogonal to the existing type toggle group, so
the two never express the same filter.
- "Errored" is driven by `last_error`, which the backend expires via
`ZO_PIPELINE_ERROR_RETENTION_MINS` (default 60 min), so the state clears
itself and cannot get stuck.

## Cluster Nodes — `web/src/components/settings/Nodes.vue`

Signal: **health**.

- Health strip doubling as the status facet, **Status** chips, and a new
**Role** column via a `nodeRole` badge group (roles were filterable but
never displayed).
- Node health previously relied on `status-row` / `status-*` row classes
that **exist nowhere in the repo**, so status rendered no colour at all.
Replaced with a real rail plus an exception tint.
- CPU/memory bars now go amber at 70% and red at 85%, and a hardcoded
literal colour (`bg-[lightgrey]`) is gone.
- Replaced the fake `#` data column (a real, draggable, resizable column
whose numbers stopped matching the rows once sorted or filtered) with
`OTable`'s `show-index` gutter, and the positional `columns.splice(2,
1)` with a filter by column id.

## Shared

- `OStatCard`: the **value no longer truncates** — a clipped `499.51…`
is a useless number, so the label absorbs the squeeze instead.
`OStatStrip` wraps a tile earlier so a five-tile strip stops squeezing
on a laptop.
- `formatEventCount` moved out of `UsageTab.vue` into
`utils/formatters.ts`, so Home and Streams cannot disagree about the
same number (`2.9B` vs `2,900,000,000`).
- Streams list endpoint: `stream_comparator` gains `doc_time_max` and
`compression` sort arms, with unit tests.

## Note on the two new sorts

`Last Ingested` and `Compression` are intentionally **not sortable in
the UI yet**. The table is `sorting="server"`, and an unknown sort key
falls through to `_ => name` with no error — a sort control would
silently order by name. Sorting them client-side isn't possible either:
server pagination means the page holds 20 of N rows. The backend arms
ship in this PR; enabling the columns is flipping `sortable: false` →
`true` once a build carrying them is deployed (the column ids already
match the keys).

## Testing

- `npm run lint`, `lint:design:strict`, `lint:tokens`,
`lint:token-purity`, `lint:styles`, `type-check` — all pass.
- 285 unit tests across LogStream, PipelinesList, Nodes, UsageTab,
formatters and badgeGroups pass.
- The Rust arms and their tests have **not been compiled locally** (no
cargo toolchain on the dev machine) — first real compile is CI.
- Cluster Nodes needs an enterprise build **and** the meta org to be
reachable (Settings → Nodes is gated `isEnterprise && isMetaOrg`).

---------

Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech>
Co-authored-by: ktx-vaidehi <134508096+ktx-vaidehi@users.noreply.github.com>
2026-07-31 10:57:51 +00:00
Abhay Padamani 1112423ea8 fix: dashboard table chart width issue and color styling issue (#13567) 2026-07-31 10:40:03 +00:00
83d12fff04 feat(web): inline-rename page titles with auto-generated default names (#13520)
## What changed

Naming an alert, panel, workflow, pipeline or function meant inventing a
name up front in a form field before anything else could be configured.
Two pieces replace that.

**Inline rename** — the page title *is* the input. `OInlineEdit` /
`OFormInlineEdit` (`web/src/lib/forms/InlineEdit`): click to rename,
Enter or blur commits, Escape restores the pre-edit value. A `#trail`
slot carries the "Auto" badge in display mode only.

**Auto-generated names** — `utils/autoName.ts` builds the name,
`composables/useAutoName.ts` decides when it's allowed to win. A name is
AUTO until the user types, then it's theirs for good; clearing the field
and blurring re-arms auto. The generators describe only what is actually
configured, so a half-built panel yields a shorter name rather than a
speculative one, and an empty string when there is nothing to say.

- Panel names read as prose — `Avg of duration by service`, `Record
count`, `k8s_logs overview`. Both live panel-field shapes are handled
(builder `functionName` + `args`, and legacy flat `column` +
`aggregationFunction`); the time column is read from
`zoConfig.timestamp_column` rather than hardcoded. Hand-written queries
fall back to the stream, since naming a panel after `x_axis_1` is worse
than saying nothing.
- Alert names are identifiers — `anomaly_k8s_logs`,
`k8s_logs_status_gte_500` — built to survive the backend's
`RE_OFGA_UNSUPPORTED_NAME` rule, with operators spelled out and a
`slugify` backstop. A leading underscore is preserved so internal
streams (`_rumdata`) keep their names.

### Supporting library changes

| Component | Change |
| --- | --- |
| `OPageHeader` / `OPageLayout` | New `titleOverflow="visible"` — the
`<h1>`'s `truncate` clips an interactive title's focus ring and the
error message it floats below itself |
| `OSelect` | New `appearance="inline"` — borderless, no fixed height,
sized to content, for a value that reads as a word inside running text
("Add Alert **in KTX**") |

All new strings go through i18n (`common.inlineEdit.*`,
`dashboard.autoName.*`, `alerts.autoName.*`, per-module `renameHint`).
Interpolated identifiers — column names, SQL functions, stream names —
stay verbatim.

## Wired into

AddPanel, AddAlert (+ `useAlertForm`), WorkflowEditor, PipelineEditor,
Functions / FunctionsToolbar, IncidentDetailDrawer,
InlineSelectFolderDropdown.

## Tested

244 unit tests across the 5 affected suites, all passing — including new
specs for `OInlineEdit`, `useAutoName`, and `autoName`.

One pre-existing expectation in `autoName.spec.ts` was updated: it
asserted `Avg of duration, max of duration +1 more`, capitalising only
the first word of a multi-measure name. `measureLabel` capitalises per
measure by design, so the expectation now reads `Avg of duration, Max of
duration +1 more`.

---------

Co-authored-by: ktx-vaidehi <134508096+ktx-vaidehi@users.noreply.github.com>
Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech>
2026-07-31 08:50:19 +00:00
Dhruv Patel e85baca92d fix: logs timechart required-fields error and URL refresh failure (#13244)
## Summary

Fixes #12897 — two defects on the Logs page **Timechart** tab:

1. The error **"Please select required fields to render the chart"**
appeared even though the chart should render.
2. **Refreshing the URL** broke the tab entirely: a *"Select \* query is
not supported for visualization"* toast plus a permanent **No Data**
state.

## Root cause

### 1. False "required fields" error (regression)

`convertPanelData()` throws this error when a chart-type panel has empty
`x`/`y` fields. The check was introduced in #10305, then **commented out
in #11297 specifically because it broke the logs visualize toggle** (the
NOTE explaining that was left in the code), and then **re-enabled in
#11586**, which reintroduced the bug.

The Timechart drives a **custom-query** panel whose axes are populated
*asynchronously* from the result schema (`resetFields()` →
`result_schema` network call → repopulate). Empty `x`/`y` is therefore a
legitimate transient state for this panel — any render that landed
inside that window (or after an extraction early-exit) hit the throw and
pushed the error into the errors panel.

### 2. URL refresh broken

Two ordering problems on page load:

- `handleBeforeMount()` restores `logsVisualizeToggle` from the URL
**before** the stream selection is restored, so the visualize toggle
watcher fired with an empty stream and built literally `select * from
"undefined"` → rejected with the SELECT \* toast (confirmed by
instrumenting the running app).
- Nothing ever re-ran the visualization after URL restoration completed
— `loadVisualizeData()` only loads stream fields — so the tab stayed on
**No Data** forever.

## The fix

| Change | File | What it does |
|---|---|---|
| Scope the empty-fields guard to builder-mode panels
(`!query.customQuery`) | `web/src/utils/dashboard/convertPanelData.ts` |
Builder panels keep the protection (the panel editor's own validation
also still blocks empty applies); custom-query panels (logs Timechart,
alerts preview) are no longer blocked by their transient empty-fields
state |
| Bail out of the visualize toggle watcher when no stream is selected
yet | `web/src/plugins/logs/Index.vue` | Stops the premature page-load
run from building `select * from "undefined"` and toasting |
| Run the visualization after URL restoration completes |
`web/src/plugins/logs/Index.vue` (`setupLogsTab`) | Mirrors the
manual-toggle setup via a shared `prepareVisualizeMode()` (quick-mode
auto-enable, layout, `customQuery`, VRL copy), restores the saved chart
type/config from `visualization_data`
(`restoreVisualizationFromUrlOnLoad()`), then triggers the run. Scoped
to the visualize toggle only — Search/build/patterns tabs untouched |
| Broaden the stream-field readiness check |
`web/src/utils/logs/visualizeStreamFields.ts` (new) | Extracted
predicate also reloads fields when quick mode is on but
`interestingFieldList` hasn't loaded yet — the state that produced
SELECT \* mid-restore |

## Verification

- **Unit tests (written failing-first against the unfixed code):** guard
skips custom-query panels / still throws for builder panels / PromQL
exempt; readiness-predicate cases. 75/75 pass across the two touched
suites.
- **End-to-end (local backend + dev UI + Playwright):** reproduced both
symptoms on the unfixed build, then verified on this branch: run →
render; chart-type switch (h-bar) → URL refresh → **chart type + config
+ query restored and chart rendered, zero errors**; VRL-function variant
renders; dashboards builder empty-fields apply is still blocked by its
validation; build-tab refresh behavior unchanged.
- `eslint` clean on all touched files (the one `no-fallthrough` error in
`convertPanelData.ts` pre-exists on `main`, untouched line); `vue-tsc`
clean.

## Before / After

Both recordings follow the identical flow: select stream →
`match_all('error')` → open Timechart → switch chart type to horizontal
bar → **refresh the URL**.

### Before (`main`) — refresh shows the SELECT \* toast and permanent No
Data

![before animation: refresh → error toast → No
Data](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/before-refresh-bug.gif)

📹 Full recording:
[before-refresh-bug.webm](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/before-refresh-bug.webm)

### After (this branch) — refresh restores chart type, query, and
renders

![after animation: refresh → chart type and query restored → chart
renders](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/after-fix.gif)

📹 Full recording:
[after-fix-full-flow.webm](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/after-fix-full-flow.webm)

<details>
<summary>Screenshots — final states and pre-refresh states on both
builds</summary>

Final state after refresh (unfixed build):

![before: refresh breaks with SELECT * toast and No
Data](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/03-before-BUG-refresh-toast-and-no-data.png)

Final state after refresh (this branch):

![after: refresh restores h-bar chart with no
errors](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/06-after-FIXED-refresh-restores-chart.png)

Timechart renders before refresh (unfixed build):

![before: timechart
renders](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/01-before-timechart-renders.png)

H-bar renders before refresh (unfixed build):

![before: h-bar
renders](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/02-before-hbar-renders.png)

Timechart on this branch:

![after: timechart
renders](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/04-after-timechart-renders.png)

H-bar on this branch:

![after: h-bar
renders](https://raw.githubusercontent.com/DHRUV6029/openobserve/pr-12897-assets/05-after-hbar-renders.png)

</details>
2026-07-31 07:45:44 +00:00
4772df1723 feat: per-group and per-series alerting, and SLO measurement (#13547)
Design at: alerts_2.md

Per-group and per-series alerting, SLO measurement, and the UI for both.

## Alerts

**Multi-alerts (Feature 3).** A grouped alert can opt in to evaluating
and paging **per group** rather than collapsing to one verdict. Opt-in
only (`multi_alert`), so no existing alert changes behaviour: the flag
cannot be present in JSON written before it existed. Per-group state,
transitions, silence fingerprints, disappearance/reaping and a
cardinality cap, with the count-gate rules that make "most severe group"
and the legacy collapsed level provably the same verdict.

**Per-series alerting for PromQL.** The same feature for the metrics
family, where a group is one returned **series** keyed by its full label
set — that is a series' identity in Prometheus, and the expression's own
`by (…)` clause is already where the label set is chosen, so a second
picker could only disagree with it. One additive nullable column; NULL
means off, so stored alerts are untouched.

**Alert detail page**, replacing the row-click side panel: stat strip,
evaluation chart with threshold marklines, per-group table and history.
Simple alerts get a real history too — evaluations (one record per run)
by default, level changes alongside.

## SLOs

Count / time-slice / alert SLIs, error-budget and burn-rate arithmetic,
rolling windows, per-group slices with an exact rollup, coverage gating,
backfill, and SLO-based alerts.

Two capabilities the model could not express, both added tests-first:

- **Metrics-native counting** (`CountSource::PromQl`) — counter metrics
have no rows for a predicate to classify, so "good" exists only as
arithmetic between series, and correct counter arithmetic is
`increase()`.
- **Freshness semantics** (`absent_is_bad`) — for a pipeline SLO,
silence IS the failure, which the default gap rule can never report. A
*failed* query still freezes rather than pages; only a query that PROVED
emptiness counts as bad.

**SLO form**: live good/bad preview so a wrong predicate is caught
before saving rather than days later, real query editors with field
autocomplete, folders shared with alerts, and a move endpoint.

## Notes for review

- The SLO and composite features are **deferred (TODO)**:
`ZO_SLO_ENABLED` stays default **false** and the SLOs menu entry is
hidden until they ship; the SLO backend and UI remain in the tree behind
the flag, and composites stay pure logic + tests.
- One migration (schema 61): a nullable column for the PromQL opt-in. No
backfill.
- Two fixes outside the feature that it surfaced: query-editor
completions were duplicated per mounted editor (Monaco registers
providers per *language*), and the alert detail chart had no PromQL
branch, so it charted row counts against the series-count gate.
- Lands together with the matching o2-enterprise PR — `QueryType::Slo`
is new here and the enterprise dedup matches are exhaustive over it.
- `ratelimit::test_add_batch_missing_rule_id` fails intermittently on
`main` as well; unrelated to this branch.

---------

Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
Co-authored-by: hengfeiyang <hengfei.yang@gmail.com>
Co-authored-by: sai nikhil kethe <nikhil@openobserve.ai>
2026-07-31 07:23:22 +00:00