mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 00:09:35 +00:00
fac24d59bb15fd3932b6bc1fd19b54b0e8a9f22e
3171
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fac24d59bb |
fix(traces): trace-log correlation fixes and Trace Graph height (#13901)
## Problem
Four defects around traces and the trace↔log correlation flows:
1. **Trace Graph is clipped.** The service map renders inside a 200px
band, vertically centred in an otherwise empty pane, instead of using
the available height.
2. **A span row's View Logs goes nowhere.** In OSS builds it navigates
to `/logs?stream=&from=NaN&to=NaN`; the logs page drops the filter and
falls back to its default window, so the user gets an unfiltered result
set instead of that span's logs.
3. **The log detail drawer has no View Trace button.** Opening the trace
for a log record only works from the inline expanded row.
4. **View Trace lived inside the JSON tab**, so it disappeared when
switching to Table.
## Root causes
**Trace Graph.** The ancestor chain was already correct — the sibling
Waterfall tab fills the same box. Only the leaf was clamped: the chart
carried `h-50! min-h-50!` (12.5rem, `!important`), which beat its own
root `h-full w-full`. That clamp dates to when the map lived in a small
collapsible timeline strip, where 200px was the intended design. When it
was promoted to a full-height tab, the scoped rule `.trace-details
.trace-chart-height { height: 12.5rem }` was left behind and its
two-class specificity silently defeated the `h-full!` added at the time.
The later design-token migration deleted the rule but preserved the
rendered result by translating it into utilities, which made the clamp
look deliberate.
**Span View Logs.** `buildQueryDetails()` reads the raw API shape
(`start_time`/`end_time`, nanoseconds), but the waterfall passes
formatted nodes, which `getFormattedSpan()` rebuilds as a fresh object
literal — camelCase, times as `startTimeUs`/`endTimeUs` in microseconds,
snake_case originals dropped. Reading `start_time` off one yields
`undefined`, hence `NaN`. The helper already had a `spanId || span_id`
fallback for the id, which is why the query string was correct while the
window was not.
**Drawer View Trace.** `JsonPreview` decides whether to offer the action
once, in `onBeforeMount`. `DetailTable` fills `rowData` in an `async
created()` — the `await` defers assignment past the child's mount even
though the work is synchronous — so the preview mounted with an empty
record, the check failed, the traces streams were never fetched, and
nothing recomputed it.
## Changes
- Trace Graph chart uses `h-full`; the orphaned `trace-chart-height`
class is dropped (its rule no longer exists) and two nested wrappers
collapse into one. Matches the full-bleed pattern in `ServiceGraph.vue`.
- The span row resolves the raw span from `spanMap` before calling the
helper — the same lookup this component already does for span data, HTTP
status and events — rather than teaching the shared helper to accept two
shapes. Guarded, since a span missing `span_id` gets a synthetic id
absent from the map.
- The span row also warns instead of navigating when no logs stream is
selected. The header's own View Logs button is disabled in that state
but the row-level one is not, so it was a silent dead end.
- `JsonPreview` re-evaluates the View Trace gate when the previewed
record changes, so the flag tracks its input rather than a single
snapshot.
- The action moves to the drawer's header row beside the wrap toggle,
reachable from every tab, with `hide-view-trace` on `JsonPreview`
(mirroring the existing `hide-view-related`). The gate, stream fetch and
filter move into `useViewTraceAction` rather than being copied into
`DetailTable` — `service_streams_enabled` is already hand-rolled in
three files and drifted. `JsonPreview` keeps exposing the same names, so
its specs are untouched.
Two latent issues surfaced while moving the action:
- `hiddenMenus` is seeded as an array and only becomes a `Set` once
`MainLayout` has read `custom_hide_menus`, so `.has()` is not always
available. Both shapes are now accepted.
- `OSelect`'s root sets `w-full`, which won a plain `w-40` and pushed
the button outside the drawer — the same class-collision shape as defect
1. Pinned with `w-40!`.
## Tests
No behaviour is asserted by new specs; existing suites cover the touched
paths and pass unmodified:
- `JsonPreview.spec.ts` — 91 passed (unchanged, including the assertions
on `tracesStreams`)
- `DetailTable.spec.ts` — 70 passed, 3 skipped
- `TraceTree.spec.ts`, `useTraces.spec.ts`,
`TraceDetailsSidebar.spec.ts` — 297 passed
- `TraceDetails` suites — 234 passed
- `SearchResult.viewTrace.spec.ts`,
`CorrelatedLogsTable.viewTrace.spec.ts` — passed
Prettier and ESLint are clean on every touched file.
## Verification
Checked against live trace and log data sharing a trace id:
- Trace Graph fills its pane (measured 549px where it previously
measured 200px), survives a tab round-trip, and reflows on window
resize.
- A span row's View Logs lands on `/logs` with the span+trace filter and
a ±60s window whose bounds match the span's real timestamps exactly;
with no stream selected it warns and does not navigate.
- The drawer's header shows the picker and button on both the JSON and
Table tabs with no overflow, and opens the trace with the correct id and
a ±15 min window.
- Console clean across every navigation.
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||
|
|
cb1b064a5d |
fix(synthetics): support to read a run's evidence without leaving the step (#13879)
## What Makes the evidence a synthetic browser check captures readable **where you already are**, instead of one tab away. Two related changes on the run-detail view: **1. An evidence row opens to its full record.** The table renders three fields the probe already captures and nothing displayed: the stack trace on a page error, the resource type, and the first-party flag. Plus the full URL — the row strips the host and then truncates, so a login URL died mid-query-string with no way to see the rest. **2. The step expansion's "Page activity" card now hosts that same table.** It used to show five severity-ranked rows and a `5 of 136` count — a card whose headline metric was its own incompleteness. It now renders the step's whole bucket with the same header, sorting, row expansion, filters, wrap and paging the Evidence tab has. Supporting work: the filter row is extracted into one component shared by both surfaces, so they cannot drift onto different controls; and the Evidence tab's step scope is a select you can change rather than a banner you can only dismiss. ## Why Every question past "was there anything?" cost a tab switch, and the correlation you arrived with — this step, this failure — had to be rebuilt by hand against a step filter. ## Notable fixes found along the way - **The Step column was sortable but sorted nothing.** It had no accessor, so the comparator read a field the row does not have, compared `undefined` to `undefined`, and left every row in place while the header showed a working arrow. - **The step expansion's content column had no width clamp.** As a flex item it kept its content's intrinsic width, so a long URL pushed it past the container and every card in it overflowed — and `truncate` and `overflow-x-auto` both went inert, since each needs a bounded parent. Measured at 1487px inside a 1000px container. - **Expand chevrons did not line up.** The row status rail's class also insets the first cell, so rows without a rail sat half a rem left. Benign rows now reserve the slot transparently. - **"Took" was never right-aligned** — the class sat on an inline span, where it aligns nothing. ## Testing `components/synthetics/`, `views/synthetics/`, `composables/synthetics/`, `utils/synthetics/` — **1398 passing**. `type-check`, `lint:design:strict` and `format:check` all clean. New behavioural coverage where it was thin: sorting is driven through the header and asserted on rendered row order rather than on column config, which is what let the non-functional Step sort ship. ## Reviewer notes - `EvidenceEvents.vue` is shared by both surfaces. The only differences left are the Step column and the page size — everything else is deliberately identical. - The step select is gated on being passed options, which is how it stays off the step card, where the step is already the scope. A test on each side pins that. - The per-step card's five-row cap is deliberately removed; pagination bounds the height instead, which bounds the card *and* keeps every event reachable in place. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
0f4b606e8e |
feat(synthetics): super-cluster support, then move synthetics to OSS (#13795)
> ### ⚠️ This PR carries four changes — review alongside openobserve/o2-enterprise#2420 > > 1. **Super-cluster support (consumer half)** — `8ceb041c43 … 5885c57b3a`. > 2. **Synthetics OSS migration** — `3a380087a3 … 2596300dbf`, nine commits — one per build-order step, plus a ninth for fixes found by running the binary. Moves synthetics out of `o2_enterprise` into `src/synthetics` (`openobserve-synthetics`), leaving only the private-VPC-agent path enterprise: `job_api::lease`, `agent/*`, private locations and the agent-setup install flow. Paired with openobserve/o2-enterprise#2420. > > 3. **Last-check columns read from the results stream** — `6af411e808`, `e7a3f2c759`. Outside the region whose scheduler runs a check, the list showed a stale status and a blank Last check / response time: `last_check_status` replicates only on change, `last_triggered_at` is a runtime column that does not replicate, and `last_response_ms` was never populated anywhere. The `synthetics_results` stream already carries all three and is federated, so the list API reads it instead of replicating the columns — no queue traffic, no schema change. Best-effort: any miss keeps the stored DB values, so search being unavailable cannot take the list down. OSS-only; no enterprise counterpart. > > 4. **Check secrets are readable in every region** — `d47a6b2345` (+ o2-enterprise `da71d7ec`). The per-org DEK is minted by each region for itself, so a check created outside the scheduler region replicated fine and then silently never ran — `resolve` could not decrypt its credentials (o2-enterprise#2451). Secrets now cross the queue **in clear** and the applier re-encrypts them under its own DEK on arrival, fixing the class at the replication boundary with no key to distribute or keep in sync. > > ⚠️ **This puts customer secrets in plaintext on the inter-region bus** — a deliberate, recorded trade-off (`o2oi_` ingest tokens and probe tokens already travel it in clear). Securing the bus itself (TLS on the gateway links, short retention on secret-carrying subjects) is the real fix and is **not** in this PR. > > ⚠️ **Deploy consumers before producers.** The payload's bytes did not change, only the meaning of the secret strings, so an un-upgraded consumer handed the new payload would write customer passwords to disk in clear. The plaintext-carrying messages therefore use new variant names (old ones retired as `CreateEncrypted`/`UpdateEncrypted`), so an old region fails serde and redelivers until upgraded instead of silently persisting plaintext. > > An OSS build can now create, schedule, dispatch to Lambda, take `resolve`/`ack` and report a check. 7 393 lines moved into the new crate; 679 lines stay enterprise. > > **🛑 Deployment prerequisite (2 of 2).** `ZO_SYNTHETICS_ENCRYPTION_KEY` must be set to the **same** base64 64-byte value in every region of a super cluster. Without it synthetics falls back to the per-org DEK and checks carrying secrets silently fail to run outside the scheduler region. It is read once at startup and pinned — changing it later requires a restart, and changing it at all orphans every secret already encrypted under the old value. > > **🛑 Deployment prerequisite (1 of 2).** 14 `O2_SYNTHETICS_*` env vars are hard-renamed to `ZO_SYNTHETICS_*` with **no fallback read**, and `ZO_SYNTHETICS_ENABLED` defaults to `false`. A deployment still setting the old names does not lose tuning — **synthetics stops running entirely, and the first symptom is checks silently not firing.** Update env vars *before* rolling the binary. > > #### Things a reviewer should look at first > > - **`super_cluster_queue` does not depend on `openobserve-synthetics`** (`cargo tree` → 0), which is what keeps the applier's loop-prevention guarantee a property of the crate graph rather than a convention. `a7d5fdd969` adds the assertion that fires if that ever changes. > - **`job_upload` was skipping authorization in OSS builds** (`4c3a0b8f0d`). Unreachable before, because no synthetics route was registered in OSS; registering it would have made it an unauthenticated artifact upload. `authorize_probe` is ungated and the bypass is gone. > - **The dependency edge had to reverse in a single commit** (`3247d4093f`). Cargo rejects an `o2_enterprise` ↔ OSS package cycle at the manifest level — `optional` and feature gating do not help — so the two edges could never coexist, and everything needed for the reversal lands together. This is why the spec's steps 4 and 5 are one commit and the config moved earlier than planned. > > #### Accepted OSS limitations, all documented in the commits > > - No notifications: `notify_check_result` ends in `alerts::alert::dispatch_notification`, which is enterprise. An OSS build runs the check and records the run; it has nowhere to send an alert, exactly as for alerts themselves. > - No per-resource RBAC on synthetics, and no OpenFGA ownership tuples — both are enterprise capabilities across the platform. > - Private locations are refused with a message rather than created. > - Agent-served public `net-*` pools are unavailable, so an OSS user needs a Lambda for every public pool (spec §1). --- Consumer half of synthetics super-cluster support. **Depends on openobserve/o2-enterprise#2420** (producer, message types, queue struct) — and **this side must deploy first**: `MessageType` bytes 83–85 hit a panicking `unreachable!()` in `From<u8>` on a consumer that does not know them, which takes down the subscriber task rather than failing one message. Based on `fix/lease-claimed-by-2270` (#13785), not `main`, so the diff is only this work. ## What this fixes Synthetics writes are not querier routes, so a create lands on ingesters in whichever region received the request and commits to that region's Postgres. There is no synthetics topic on the super-cluster queue, so a check created in a region that runs no scheduler is never seen by the region that does: the UI shows it enabled, the scheduler has never heard of it, and nothing reports the gap. ## Commits | commit | what | | --- | --- | | `8ceb041c43` | orphan-detection query + metrics | | `1c3e10da48` | apply replicated check config | | `79e6abc052` | apply replicated locations and probe tokens | | `16d4fa0fde` | render unknown live status instead of a false pending | | `5885c57b3a` | share the job-cluster predicate | ## Design notes worth reviewing **Config columns only, and the list is not restated.** `synthetics_checks::update` rebuilds its `ActiveModel` from the row already in this region, and `update_mutable_fields` writes only user-editable columns — so `next_run_at` and the alerting counters keep local values for free. Hand-listing them in the consumer would duplicate a list that already exists and let the two drift. A test asserts every runtime column comes back `Unchanged`. **`put()` is deliberately unused.** Its insert branch builds from `build_active_model`, which ends in `..Default::default()` and never assigns `owner`, `tz_offset` or `next_run_at` — a replicated create through it would drop the owner and fire a scheduled-later check immediately. **`create()` gains `use_given_id`**, matching `table::alerts::create`. It generated its own uuid, which would have made every region invent a different primary key for the same check. **Consumers apply through `infra::table` only.** The publish lives one layer up in the enterprise service, and `infra` cannot reach the enterprise crate — that layering is what stops a region re-broadcasting what it just applied. `source_cluster` does not cover this: it only stops a region re-consuming its *own* message. Each consumer greps its own source to keep this true. **Runtime tables are never replicated** — `synthetics_jobs`, `synthetics_runs`, `synthetics_agents`, and the locations `down_notified_at` claim. Asserted at both producer and consumer. ## Known gaps, named not hidden - If two regions mint a token (or location) with the same name for one org before either message crosses, the UNIQUE constraint rejects the arriving row. Local row wins, logged loudly. Erroring would redeliver to dead-letter, which is worse. - Pre-existing and untouched: `write_triggers_stream`/`write_results_stream` in the enterprise reaper post to `_meta` with the *org's* ingest token, which 401s, and neither checks response status. Orphan detection fixes this in its own path only. - Pre-existing: `create` never sets `tz_offset`, so a new check gets DB default 0 until its first edit. Replicates identically everywhere, so it causes no divergence. ## Spec corrections The spec (`docs/synthetics-lcl/2026-08-11-p1-3-super-cluster-spec.md`, local notes) is wrong in five places, all found by reading the tree: §1's invariant evidence (see the enterprise PR), §10's never-run guard, §4's column list (and it lists six runtime columns — there are seven, `alerting` is missing), §3's "private rows read-only when remote", and §7's claim that non-scheduler regions render OFFLINE (they render `pending`). ## Verification infra 42, super_cluster_queue 32, jobs 21, web 111/111 vitest + type-check + design lint, `cargo fmt --check` and clippy clean, full `--features enterprise` build. Pre-existing failures on this branch, unrelated and untouched: `super_cluster_queue::schemas::tests::test_parse_key_{valid,metrics}`, `infra::table::ratelimit::tests::test_add_batch_missing_rule_id`, and 8 web vitest failures in `buildPayload.spec.ts`/`payloadContract.spec.ts`. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d6c726c222 |
fix(synthetics): show where recorded steps will land (#13860)
## What Shows where recorded steps will land in a browser journey — as a labelled divider above the target row, previewed on hover and held for the whole recording session. ## Why Each step row carries a Record control meaning "restore the journey up to here, then record new steps before this row". It was the most expensive click in the editor — it opens an incognito window and replays every preceding step — and nothing said where the result would land until after that had already started. The only signal was a "Recording here" badge rendered inline in the anchored row's details cell, between the action icon and the action label. Sitting inside the row, it read as *this row is recording* when it means *new steps go above this row*. ## What changed - The badge is replaced by a divider on the row's top edge: two rule segments with a centred label between them, straddling the boundary so it names a position **between** rows rather than a property of one. - Hovering or focusing a row's Record control draws the same marker in a lighter tone, so the destination is legible while the click is still free. - The preview is suppressed wherever the control is disabled — the first row, a locked table, or an extension too old to restore. A preview of an action you cannot take is worse than no preview. - Focus is bound alongside hover, so the affordance is not mouse-only. Two edge cases surfaced while testing and are fixed here: - A replay started from the toolbar locks the table while the pointer rests on a row control. Nothing moves, so no `mouseleave` arrives, and the preview sat on a locked table offering a click that was now refused. - Both render modes share the details cell slot, so an anchor id reaching results mode drew a marker on a finished run — over a cell with no positioning to hold it. ## Implementation notes No `OTable` change. The marker is absolutely positioned against the details `<td>`, which becomes a containing block via the column's `meta.cellClass`. Two consequences worth knowing: - **Nothing reflows.** Toggling the marker on hover repaints only. A rule that took layout would jitter the whole table as the pointer crossed the action column. - **The gap is real, not painted.** The label sits between two `flex-1` segments rather than over a continuous rule. That centres it without measuring anything, and means it needs no background to avoid being struck through. `getCellStyle` lifts the cell's `overflow: hidden` on the marked row only, so the label can overhang the boundary. Step-name truncation is unaffected — that ellipsis lives on the inner wrapper, not the `<td>`. ## Testing - `JourneySteps.spec.ts`: 43 tests (18 new), covering the committed marker, the hover and focus preview, suppression on every disabled path, tone precedence when a committed anchor and a hover coexist, anchor reassignment, the positioning contract, and results mode. - Full journey suite green: 336 passed, 1 skipped, `BrowserJourney.spec.ts` unmodified — which is what shows the marker rides the existing `anchorId` prop rather than duplicating state. - Guards were mutation-checked: deleting each one turns the suite red, so none of them pass vacuously. - `lint`, `type-check`, `format:check` and `check-design-consistency.mjs` all clean. ## Not covered jsdom has no layout, so positioning is pinned by class assertions only — this has not been verified visually. Worth an eye on the label at the dense row height, in both themes. Out of scope, and unchanged by this PR: - The marker does not survive into recording — `BrowserJourney` replaces the whole journey table with a capture-only table while recording, so there is no row to mark. - The toolbar Record button draws no marker below the last row. - Downstream steps keep stale pass ticks after a splice, and the passed banner recomputes its total from the new step count. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
4b6491a0ea |
fix: session detail output message fixed (#13889)
## Summary - web/src/plugins/traces/SessionDetails.vue - web/src/plugins/traces/composables/useSessions.ts - web/src/plugins/traces/threadView.utils.ts (new) - web/src/plugins/traces/threadView.utils.spec.ts (new) ## Test plan - [x] `npx vitest run` on `threadView.utils.spec.ts` - 114 passed - [x] `vue-tsc --noEmit` (`type-check:app`) - clean --------- Co-authored-by: sai nikhil kethe <nikhil@openobserve.ai> Co-authored-by: Shrinath Rao <shnath@openobserve.ai> |
||
|
|
84b666ce0b |
fix: annotation_queues item hydration intermittent 500s (#13881)
## Summary
RCA (2026-08-19, 08:20-08:22 UTC): `GET
/api/{org}/annotation_queues/{queue_id}/items/{item_id}` (and its
`/reviews` sibling) intermittently returned 500. Originally attributed
to search work-group admission capacity exhaustion; tracing the actual
code found a different root cause.
**Real mechanism**: `eval_jobs::search::execute()`'s registered
executors (in `src/jobs/src/job/mod.rs`) called
`search_service::search("", ...)` with a hardcoded empty `trace_id`.
With no propagated OTel span in that detached task,
`search_service::search()` falls back to OpenTelemetry's *invalid* trace
ID for every such call - the same `000...000` seen in every failing log
line. Every concurrent internal search therefore shared one
`QueryManager` map key: one query's cleanup (`SEARCH_SERVER.remove`)
deleted another's still in-flight registration, so the next
`insert_sender` failed with "trace_id not found" -> `SearchCancelQuery`
-> 500. `work_group_checking` never reached the real concurrency-wait
branch in this failure mode - "work-group capacity" was never the actual
mechanism.
## Changes (3 commits)
1. **`fix(annotation-queues): merge item detail and reviews into one
call`** - the Queue Workbench always fetches item detail + reviews
together (confirmed: no caller ever needs one without the other,
including after review submission); fold them into one backend call,
cutting the per-item load from 2 HTTP round trips to 1. See companion PR
openobserve/o2-enterprise#2473 for the response-shape change this
depends on.
2. **`fix(annotation-queues): map search admission cancellation to
429`** - `annotation_queue_error_response` unconditionally 500'd on
`AnnotationQueueError::Search`/`Hydration`, even when the underlying
cause already has a defined 429 `http_status()` (`SearchCancelQuery` /
`RatelimitExceeded`). Downcast and map correctly instead.
3. **`fix(search): stop colliding on trace_id in internal eval
searches`** - the actual root-cause fix: generate a real per-call
`trace_id` via `config::ider::generate_trace_id()` at all three internal
`search_service::search("", ...)` call sites instead of passing `""`.
## Dependency
This PR's enterprise build depends on openobserve/o2-enterprise#2473
(same branch name `ud/fix-annotation-queue-hydration-500s`) via the path
dependency in `Cargo.toml.openobserve` - that PR adds the `reviews`
field on `AnnotationQueueItemDetail` this PR's merge (commit 1)
consumes.
## Test plan
- [x] `cargo build --features enterprise`
- [x] `cargo test -p openobserve-api-management --lib --features
enterprise annotation_queue` - 11 passed (incl. 2 new tests for the 429
mapping), no regressions
- [x] `cargo test -p o2_enterprise --lib annotation_queues` (companion
repo) - 16 passed, no regressions
- [x] `cargo clippy --features enterprise -p openobserve-jobs -p
search_service -p openobserve-api-management --no-deps` - clean (no
warnings in touched files)
- [x] `cargo fmt --check` - clean
- [x] Frontend: `npx vitest run` on `llm-queues.service.spec.ts` +
`QueueWorkbenchPage.backTarget.spec.ts` - 9 passed
- [x] `vue-tsc --noEmit` (`type-check:app`) - clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
be67ed442a |
fix(deps): add missing @noble/hashes@2.3.0 to web lockfile (npm 10 npm ci) (#13872)
## Problem
Every frontend build is red on `npm ci` in `web/`:
```
npm error `npm ci` can only install packages when your package.json and
package-lock.json ... are in sync.
npm error Missing: @noble/hashes@2.3.0 from lock file
```
## Root cause (not a feature change)
- `web/package-lock.json` was last regenerated with **npm 11**, whose
resolver **omits** the explicit `@noble/hashes@2.3.0` entry that
`jsdom`'s transitive `@exodus/bytes@1.15.1` **peer-requires** (`^1.8.0
|| ^2.0.0`).
- The **Playwright build pins `node-version: 22` → npm 10**
(`.github/workflows/playwright.yml`). npm 10's `npm ci` recomputes the
ideal tree, needs that entry, finds it missing, and hard-fails.
- Reproduced locally: `npm@10 ci` **fails**, `npm@11 ci` **passes**, on
the exact committed lock. So it's a **toolchain mismatch**, independent
of any PR — it breaks `main` and every open PR building the frontend.
## Fix
Regenerated the lock with `npm@10 install --package-lock-only` → adds
the nested `node_modules/jsdom/node_modules/@noble/hashes@2.3.0` entry.
**+15 lines, lockfile only**, `lockfileVersion` unchanged (3).
Verified `npm ci` now passes under **both npm 10 and npm 11**.
## Note
The durable fix is aligning the Playwright build to **Node 24 / npm 11**
(matching the AI-review workflow and the Node 20/22 runner deprecation).
This PR is the minimal unblock; the Node bump can follow separately.
|
||
|
|
351b356620 |
feat(alerts,slos): export as Terraform and OpenTofu (#13849)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
ecc1d5dbff |
fix(ui): clean up SLO detail and traces navigation (#13852)
## Summary This PR combines the SLO detail cleanup and the Traces navigation fixes into one change set. ### SLO cleanup - remove the internal definition generation value from the SLO detail title - remove the unused translation key and update generated locale files - normalize SLO health icon identifiers - avoid rewriting unchanged locale files during translation updates ### Traces navigation - add Spans as the first item in the Traces flyout - route Spans, Traces, Service Graph, and Service Catalog through /traces?tab=... - redirect legacy Service Graph and Service Catalog paths while preserving query state - support same-route menu switching and retain stream, time-range, and query context - remove the deprecated search_mode parameter from generated and legacy URLs - keep Traces as the default for tabless URLs ## Validation - focused Traces Vitest suites: 436 passed, 2 skipped - app TypeScript check - full frontend ESLint check - Prettier checks - live browser verification of Traces and Spans menu switching --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
7dd19d78ad |
fix: reduce eval match preview window to one hour (#13859)
## Summary - reduce the new evaluation job matched-target preview from a rolling 24-hour window to one hour - update span, trace, and session preview labels to show `last 1h` - add deterministic coverage for the one-hour query range ## Why Selecting a stream immediately runs a matched-target count query. The hard-coded 24-hour window can scan a large amount of trace data on high-volume streams even though this number is only a creation-time preview. ## Impact This only changes the matched-target preview query and its label. It does not change the evaluation job schedule or execution range. ## Root cause The preview composable and its labels were hard-coded to 24 hours. ## Validation - `npm run test:unit -- --run enterprise/components/onlineEvals/composables/useJobMatchedTargets.spec.ts enterprise/components/onlineEvals/forms/job/JobPreviewPanel.spec.ts` - `npm run type-check:app` - focused ESLint and Prettier checks - `npm run lint:design:strict` ## Companion PR - openobserve/o2-enterprise#2452 fixes the OpenAI token-limit parameter and raises the LLM judge defaults. |
||
|
|
a6c910f015 |
fix(web): logs scroll jump, dashboard tab-rename width, and banner usage (#13858)
Three unrelated UI fixes. ### `fix: logs page wrap scroll` The Logs view delegates its scroll element to a container that also holds the histogram. TanStack Virtual compensates `scrollTop` whenever a measured row differs from its estimate — correct for a self-contained list, wrong for a shared scroller. With wrap on, rows were remeasured on every query refresh and the deltas pushed the whole page downward. Adds `preserveScrollOffsetOnRowResize` to `useTableVirtualization`, which pins `shouldAdjustScrollPositionOnItemSizeChange` to `() => false`. `OTable` opts in only when it has both a delegated `scrollEl` and dynamic row height, so self-contained tables keep the existing behaviour. ### `fix: dashboard tab rename size issue` The rename input's auto-sizer grid track was `minmax(0, max-content)`. Once the tab strip overflowed, the `0` minimum collapsed the track to `0px` and the input vanished. Now a bare `max-content`. ### `fix: ingestion and webinar obanner usage` Replaces two hand-rolled banner `div`s (`Ingestion.vue`, `WebinarBanner.vue`) with `OBanner`. To make that possible without appearance overrides, `OBanner` gains: - a `promo` variant (the existing marketing gold, so the standalone webinar bar still matches); - `center`, which groups message and actions in the middle of a bar instead of stranding the action at the far edge; - a `role` override, for a bar that is page furniture rather than a notification; - `dense` now actually applies in `bar` mode (`py-1` vs `py-2`). ### Tests `TabList.spec.ts` and `OTable.wrapMeasure.spec.ts` updated alongside. |
||
|
|
c53a16b262 |
fix: script i18n (#13840)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
71ac1071be |
fix: draft workflows (#13666)
Signed-off-by: Yashodhan Joshi <yjdoc2@gmail.com> Co-authored-by: Yashodhan Joshi <yjdoc2@gmail.com> Co-authored-by: Yashodhan Joshi <yashodhan@openobserve.ai> |
||
|
|
99b1a23773 |
test(slo): E2E + API suite for SLOs (#13836)
Adds automated test coverage for **SLOs** (#13547, #13577, #13579, #13704, #13761, #13784), which shipped with none. **149 tests** — 77 Playwright + 72 pytest — all three SLI types, both query languages, happy / negative / edge paths. Clean branch off `main`. **UI (Playwright · `tests/ui-testing`)** - `pages/sloPages/` — 4 page objects (verified `data-test` selectors, hydration + Monaco-sync waits, OSelect commit-retry, virtualized-list filtering). - `playwright-tests/SLO/slo-crud.spec.js` (24) — create count/time-slice/grouped through the form, detail, edit, SLI-type change, tags, pause, delete, filters; negative: dismissed delete, empty search, cancel, unmeasured SLI is absent not zero. - `playwright-tests/SLO/slo-form-validation.spec.js` (20) — server errors surfacing in the form, conditional fields per SLI type, count + time-slice previews, **PromQL count and time-slice created end to end**; negative: no stream, invalid aggregate, over-long name, target boundaries. - `playwright-tests/SLO/slo-alert-sli.spec.js` (8) — **the `alert` SLI type**: source picker, create, config carries only the alert id, detail names the source; negative: ineligible source offered-but-disabled, grouped alert SLI, slice finer than source cadence. - `playwright-tests/SLO/slo-timeslice.spec.js` (10) — **#13761 P0 regression** (measured SLI, not the preview), PromQL, previews, gaps, language-flip clearing. - `playwright-tests/SLO/slo-alerts.spec.js` (15) — burn-rate + error-budget kinds, presets, edit, freeze door, burndown; negative: no destination, cancel, blank/invalid name. - `playwright-tests/utils/slo-seed.js` — backdated seeding, metrics seeding, eligible-source-alert fixture, ground-truth query, fail-fast env guard, transient-store retry, full fixture teardown (alerts → streams → destinations → templates). **API (pytest · `tests/api-testing`)** - `tests/slos/test_slo_validation.py` (72) — full REST surface (create/get/update/enable/delete/groups/slo-eligible), validation on **both create and update** (name, target, window, slice, config shapes, comparators, float-threshold round-trip), boundaries (target 0.001/99.999, name 256 vs 257, threshold 0/negative/1e12/NaN, unicode names), alert-source eligibility (silence-gated, too-infrequent, cron), PromQL config shapes, auth 401/404. **CI** - Two shards: `SLO` (`workers: 3`) and `SLO-Measurement` (`workers: 1`). Split because the backfill job runs with `ZO_SCHEDULER_SLO_BACKFILL_CONCURRENCY=1`, so measurement specs sharing an instance starve each other's waits — `fullyParallel` races spec *files*, which `mode:'serial'` cannot prevent. The 3-worker pin is SQLite: at 5, concurrent SLO/alert/destination writes trip `database is locked` on the single-node meta store. - New optional per-shard `ingest_allowed_upto` (`240`) alongside the existing `quick_mode_enabled`: the workflow default of 5 **hours** silently drops the 8 days of history an SLO window needs, while still answering `200`. Other shards keep the default. Same precedent as `api-testing.yml`'s `48`. **Feature code**: 9 additive `data-test`/`dataTest` attributes in `SloDetail.vue`, `SloList.vue`, `SloAlertCondition.vue` — no logic changes, `vue-tsc` clean. The stat tiles, tabs and burn-window inputs had no stable selectors; `OStatStrip`/`OTab` already forward the attribute. **Behaviour**: self-cleaning (`e2e_slo_*`, worker-scoped since `afterAll` runs per worker). **Verified in CI** on this branch: `e2e / SLO` **52 passed** (1.8m, workers pinned to 3), `e2e / SLO-Measurement` **25 passed** (11.3m, workers pinned to 1) — the measurement shard passing confirms the per-shard `ingest_allowed_upto` took effect, since those tests are impossible under the 5-hour default. pytest **72 passed** locally; in CI's 599-test run one setup delete hit a transient 500 (see findings) and now retries. **Findings for the feature owner** (reported, not fixed here): - A blank "good when" shows `Request failed with status code 422` — `pruned()` drops the empty key, so the API's real 400 message never reaches `save()`. - A rejected alert name shows no banner: `SloAlertForm.submit()` returns on `nameError` before setting `saveError`. - `avg(latency_ms); DROP TABLE users--` is accepted at create time (parsed only at measurement). - The name-length message says "less than 256 characters" but 256 is accepted — 257 is the first rejection. - Duplicate alert names on one SLO are accepted (duplicate SLO names are a 409), and the panel lists them by name, so they are indistinguishable. - SLO names accept characters that break the list's own `slos-slolist-edit-${name}` selectors; alert names correctly reject them. - All chart/preview root `data-test` ids are unreachable — a parent's fallthrough attribute overrides the child root's. - Both edit forms hydrate asynchronously, so a blank form can flash before the stored values appear. - `DELETE /slos/{id}` on a freshly-created SLO returned **500** once under CI load (not reproducible locally in 12 rapid iterations). Contention is arguably a retryable 503, and the body carried no actionable message. - **A scheduled alert whose source stream is deleted is retried forever** (`Stream "…" not found in schema`), and the SLO backfill lane shares the scheduler's worker budget — so accumulated dead alerts *starve SLO measurement*. Observed: 65 of them held coverage at 1 of 7 chunks indefinitely; deleting them restored 0.143 → 1.0 in 240s. Harmless in CI (fresh instance per shard) but it degrades any long-lived environment. The suite now cleans up after itself; the retry-forever behaviour itself may deserve a look. **Paired ENT PR: openobserve/o2-enterprise#2434.** ENT's matrix is this repo's manifest plus an overlay, merged by this repo's `build-ci-matrix.js`, so both SLO shards reach ENT automatically — verified: 31 merged shards, both SLO entries carrying `ingest_allowed_upto=240` and their worker pins, no other shard affected. What ENT was missing is *consuming* those two fields, which #2434 adds. Merge the two together: this one alone leaves ENT booting the SLO shards on the 5-hour ingest window, and #2434 alone is inert. *(An earlier review reported ENT as using an inline matrix with no SLO shard — that turned out not to be the case.)* |
||
|
|
69bd367d9b |
feat: notification dependency view — where-used for templates, destinations & alerts (#13820)
## What Adds a **"View dependencies"** affordance to the **Templates**, **Notification Destinations**, and **Alerts** lists. A small `graph-1` icon on each row opens a compact popover that shows everything that entity connects to along the **Template → Destination → Alert** chain, with search, pagination, and per-row **Open** / **Delete**. ## Why The linkage between alert templates, destinations, and alerts is invisible today. There is **no reverse-lookup API** — the only way to learn "which alerts use this destination" is to try deleting it and hit the backend's `409 "Destination is currently used by alert: X"`. That's a dead end, not a map. This surfaces the dependency proactively so you can see the blast-radius *before* you change or delete something, and act on it in place (open or delete a related entity, with the same `409` guard still protecting in-use items). ## Design The three entities are linked **by name** (there are no ID foreign keys): - `Alert.destinations: string[]` → `Destination.name` - `Destination.template` → `Template.name` (a destination's default) - `Alert.template` → `Template.name` (an alert-level override) ```mermaid flowchart LR T["Template"] -->|"Destination.template"| D["Destination"] D -->|"Alert.destinations[]"| A["Alert"] T -. "Alert.template (override)" .-> A ``` Because the alerts **list** DTO previously omitted `destinations`, the whole graph would have needed N per-alert detail fetches. Instead a small **opt-in additive field** on the list item makes the entire graph **three list calls**, cross-referenced client-side and cached per-org: ```mermaid flowchart TD Row["List row · graph-1 icon"] --> Pop["DependencyChainPopover<br/>(OPopover + ConfirmDialog)"] Pop --> Panel["DependencyUsagePanel<br/>search · collapsible sections · pager"] Panel --> Rows["DependencyUsageRow ×N<br/>Open · Delete"] Panel --> Load["useDependencyGraph.loadGraph()<br/>per-org cache"] Load -->|"alerts?include_dependencies=true<br/>+ destinations + templates (3 list calls)"| Build["buildGraph() → nodes + edges"] Panel --> Focus["buildFocusChain(graph, focus)<br/>directional: a shared template never<br/>drags in sibling destinations"] Rows -. "requestDelete(node)" .-> Pop Rows -. "open(node) — same-kind, in-place" .-> Pop Pop -. "ConfirmDialog → service.delete()<br/>(409 'used by' → toast)" .-> Done["emit deleted → list refreshes"] ``` Why the ConfirmDialog lives in the popover **wrapper**, not the panel: a modal can't render inside the popover without dismissing it, so the row/panel only *request* a delete and the wrapper owns the real confirmation. **Open** works the same way for the in-place editors: templates/destinations edit on their own list route (which only opens from the page's mount-time handler), so a same-page open is bubbled to the host page to invoke its native editor rather than a no-op `router.push` to the same route. ## UX / behaviour - **Where-used, grouped:** collapsible **Templates · Destinations · Alerts** sections, each a clickable row (kind-coloured icon, name, and for destinations an "N alerts" **total-usage** badge). - **Search-first:** one search box filters all groups — the key affordance when a destination feeds thousands of alerts. - **Paged:** the alerts group renders **50 at a time** with a pager pinned beside its (sticky) header, so 4,000-alert destinations stay responsive. - **Act in place:** every row has **Open** (templates/destinations → their editor; alerts → the alert's detail page) and **Delete** (the same `ConfirmDialog` the list pages use; the backend `409` still blocks in-use items). - Consistent trigger on all three lists; **purely additive** — no existing button, icon, or column was moved or replaced. ## Backend change `ListAlertsResponseBodyItem` (`src/api/management/src/models/alerts/responses.rs`) gains two additive fields — the linkage the dependency view cross-references: - `destinations: Vec<String>` - `template: Option<String>` (the alert-level override) They serialize **only when the caller passes `?include_dependencies=true`** (which just the dependency popover does); the default paginated alerts-list path is unchanged, so ordinary consumers pay no extra bytes and receive no module-scoped destination/template names. Both are also `skip_serializing_if`-empty. Backward-compatible, OSS, no new endpoint. ## Testing - **Unit (vitest) — 17 tests:** - `useDependencyGraph.spec.ts` — `buildGraph` (linkage, unused/orphan, dangling refs, override edges, shared destinations) and directional `buildFocusChain` per focus kind (incl. the alert-focus total-usage count). - `DependencyUsagePanel.spec.ts` — sections render every kind, search + 50/page paging, collapse, `requestDelete`, the same-kind `open` bubble, and the `include_dependencies` opt-in. - `DependencyChainPopover.spec.ts` — trigger + lazy panel mount, `requestDelete` → ConfirmDialog → API + `deleted`, and the `open` bubble + close. - **Backend:** serialization test asserts `destinations`/`template` are omitted when empty. - `type-check`, `lint:design:strict`, and `prettier` all clean. - A popover e2e (Playwright) is a follow-up in a separate PR — the earlier spec targeted an interim dedicated page that this PR does not ship, so it was removed. ## Notes - Net change is popover-only: an earlier iteration added a dedicated Reliability page + route + rail entry; those were dropped in favour of the on-the-fly popover, so `router.ts` / `navGroups.ts` are untouched vs `main`. - Icon `graph-1` was added to the icon registry (additive) for the popover trigger. - **Deferred (see design doc Non-goals):** a server-side where-used / `?destinations=` endpoint so the popover fetches only the neighbourhood instead of the full alert list; and enterprise RBAC name-filtering on the gated fields. Design at: https://github.com/openobserve/designs/blob/main/alerts/notification-dependencies/design.md (designs#8: `alerts/notification-dependencies/design.md`) --------- Co-authored-by: Abhay Padamani <141122205+ktx-abhay@users.noreply.github.com> Co-authored-by: ktx-abhay <abhay.padamani@kiara.tech> |
||
|
|
1c849a9c71 |
refactor(web): migrate templates + shadows to tokens, delete the debt baseline (#13769)
## What
Deletes `web/scripts/design-debt-baseline.json` — **not by relaxing the
rules, by fixing the debt**. Every design-token category is now
zero-tolerance, SCSS is gone from the app, and 144 hand-written shadow
values collapse into one scale.
## The guard was under-reporting (three separate holes)
1. **Comment contamination.** `check-design-consistency.mjs` fed comment
text into its exemption checks, so a `keep()` comment that merely
*named* `:deep(` / `::after` / `@keyframes` silenced the rule after it —
and a `;` or `{` inside a comment corrupted the offsets, so stripping
comments afterwards couldn't help. Comments are now blanked
(offset-preserving) before the scan. **Uncovered 9 occurrences in 6
files.** Real debt was 110, not 101.
2. **`arbShadow` only scanned the template.** `box-shadow:` in a
`<style>` block and `boxShadow:` in a JS/`:style` object were invisible
— 40 literals sat there while the guard reported zero. It now scans all
three syntaxes across `.vue` and `.ts`.
3. **`rawVarInTemplate` missed the fallback form.**
`border-(--color-x,rgba(…))` and `bg-[var(--color-x,#fff)]` slipped
through. The token is always defined, so the fallback can never fire —
it only hid the read.
## stylelint had never run on 14 files
`postcss-scss` isn't installed, so `postcss-html` **silently skipped**
every `<style lang="scss">` block. The CI step named *"Lint styles
(stylelint — bans legacy --o2-* tokens)"* was a no-op for those files —
including `color-no-hex` and the `.body--dark` selector ban. All 14 are
plain CSS now and `vue/block-lang` errors on `scss`/`sass`/`less`, so it
cannot silently recur.
## What moved
| | before | after |
|---|---|---|
| `rawVarInComponent` (in `<style>`) | 110 | **0** |
| template `x-[var(--color-*)]`, utility exists | 225 | **0** |
| template reads, no utility | 27 | **0** |
| `style` / `:style` colour literals | 29 | **0** |
| `color-mix(token N%, transparent)` | 160 | **0** |
| hand-written shadows | 233 (144 distinct) | **0** |
| SFCs using SCSS | 14 | **0** |
Guard categories: 22 → 24. `rawVarInTemplate` and `arbShadow` are new;
`arbPx` was deleted because main's `local/no-hardcoded-px` ESLint rule
(#13541) now owns px across every file type.
## Shadows are a system now
Stripped of colour, the 144 values were the same handful of geometries
rewritten in px vs rem, with and without an explicit `0` spread.
```
--shadow-xs / sm / md / lg elevation
--shadow-sticky-left / -right directional (sticky columns)
--shadow-rail inset status rail
--shadow-glow-sm / -md / -lg coloured, 3 intensities
--shadow-scroll-top / -bottom / -both scroll affordance
--shadow-ring-hairline 1px ring where a utility can't reach
--shadow-ai-focus AI composer focus glow (flips in dark)
```
Colour is a **separate axis**. Tailwind rewrites a `--shadow-*` token as
`<geometry> var(--tw-shadow-color, <default>)` — verified by compiling a
probe, and it holds for multi-layer tokens — so a template writes
`shadow-md shadow-ai-accent/35`. A plain stylesheet can't add a utility,
so the role and glow tokens read `--glow-color`; that keeps the pipeline
canvas colour-coding nodes by type from CSS and lets the status rail be
painted per row from JS.
Also reclassified: `0 0 0 2px <colour>` is a **ring**, not a shadow, and
`0 1px 0 0 <colour>` is a **border**.
## color-mix, analysed by intent
All 331 occurrences were classified rather than treated as one problem:
- **160** mixed a token with `transparent` — only an alpha change, so
it's Tailwind's `/N` modifier written the long way (verified: `/N`
compiles to `color-mix(in oklab, <token> N%, transparent)`, and mixing
with `transparent` is premultiplied, so the interpolation space doesn't
matter)
- **81** blended two colours — a derived colour, so it's a token now
(~50 minted)
- **75** were shadow/ring geometry, handled by the scale
- **8 gradients + 1 `calc()`-driven percentage** genuinely have no
utility form and remain; the guard exempts gradient/mask/filter
contexts, matching what the style-block counter already exempts
## Merge with main
Main was running the same migration in parallel (**#13541 "px to rem"**
removed all SCSS, added `no-hardcoded-px`, and built its own shadow
scale), so the merge collided in 68 files / 155 hunks. Resolved by
taking main's source files and re-applying this branch on top, with the
guards driving convergence (429 → 0) rather than hand-resolving hunks.
On shadows, **main's structure wins** — geometry in `semantic.css`
`@theme`, colours as `--shadow-*-c*` sub-tokens, wrapped in its px
disable; its `--shadow-sm/md/lg`, `--shadow-scroll-*` and
`--shadow-glow` keep their definitions. This branch adds what main
didn't have: `xs`, the directional/inset roles, and the
elevation-matched coloured glows.
## New tokens
No new *colours* — each names a value a component already hardcoded,
because a registered palette utility (`text-orange-500`) is banned by
`rawPalette`, so "just register the ramp" was never available.
`--color-splitter-hover`, `--color-status-flaky` (+`-text`),
`--color-chat-bubble-user-border`, `--color-thread-avatar-bg`,
`--color-thread-accent-strong`, `--color-menu-badge-ring`,
`--color-progress-track-fill`, `--color-ingest-accent-*` (5), ~50
derived blend tokens, plus `--color-border-strong` /
`--color-border-subtle` / `--color-warning-300` registered for utility
use.
## Visual deltas to review
Everything else is value-preserving. These are not:
- **4 hover accents** move from a fixed `primary-600` to `accent` — 600
→ 400 **in dark only** (2 chevrons, an empty-state icon fill, one link)
- **sticky table header** in `TraceDetailsSidebar` was a fixed light
grey that never flipped; now `border-default`, so it themes
- **shadow canonicalization** snaps near-identical geometries together
and moves ~40 hardcoded `rgba(0,0,0,x)` onto the scale's token colour
Highest-surface single file: `AlertSettingsHelpDrawer.vue` — a whole
style block deleted with ~44 class names moved into template utilities.
## Verification
`lint:design` · `lint:tokens` · `lint:token-purity` · `lint:styles` ·
`format:check` · `lint:ci` · `vue-tsc` — all clean.
**94 spec files covering every file this branch touches: 4395 passing, 0
failing.**
Not run: the full 1236-file unit suite, Playwright/E2E, and any browser
visual check.
---------
Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech>
Co-authored-by: Abhay Padamani <141122205+ktx-abhay@users.noreply.github.com>
Co-authored-by: ktx-abhay <abhay.padamani@kiara.tech>
Co-authored-by: ktx-vaidehi <134508096+ktx-vaidehi@users.noreply.github.com>
Co-authored-by: Bhargav <BJP232004@GMAIL.COM>
|
||
|
|
f112ac01a5 |
fix: dashboard layout improvements (#13815)
<h2 style="unicode-bidi: plaintext; color: rgb(204, 204, 204);
font-family: -apple-system, "system-ui", "Segoe UI",
Roboto, sans-serif; font-style: normal; font-variant-ligatures: normal;
font-variant-caps: normal; letter-spacing: normal; orphans: 2;
text-align: start; text-indent: 0px; text-transform: none; widows: 2;
word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal;
background-color: rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color:
initial;">Dashboard layout & UX improvements</h2><p
style="white-space: pre-wrap; margin-top: 0.1em; margin-bottom: 0.2em;
unicode-bidi: plaintext; color: rgb(204, 204, 204); font-family:
-apple-system, "system-ui", "Segoe UI", Roboto,
sans-serif; font-size: 13px; font-style: normal; font-variant-ligatures:
normal; font-variant-caps: normal; font-weight: 400; letter-spacing:
normal; orphans: 2; text-align: start; text-indent: 0px; text-transform:
none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;
background-color: rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color: initial;">Polish
pass on the dashboard experience: settings layout, drag/resize grid
feedback, empty states, and per-tab panel counts.</p><h3
style="unicode-bidi: plaintext; color: rgb(204, 204, 204); font-family:
-apple-system, "system-ui", "Segoe UI", Roboto,
sans-serif; font-style: normal; font-variant-ligatures: normal;
font-variant-caps: normal; letter-spacing: normal; orphans: 2;
text-align: start; text-indent: 0px; text-transform: none; widows: 2;
word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal;
background-color: rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color: initial;">✨
Highlights</h3><p style="white-space: pre-wrap; margin-top: 0.1em;
margin-bottom: 0.2em; unicode-bidi: plaintext; color: rgb(204, 204,
204); font-family: -apple-system, "system-ui", "Segoe
UI", Roboto, sans-serif; font-size: 13px; font-style: normal;
font-variant-ligatures: normal; font-variant-caps: normal; font-weight:
400; letter-spacing: normal; orphans: 2; text-align: start; text-indent:
0px; text-transform: none; widows: 2; word-spacing: 0px;
-webkit-text-stroke-width: 0px; background-color: rgb(24, 24, 24);
text-decoration-thickness: initial; text-decoration-style: initial;
text-decoration-color: initial;"><strong>Grid feedback while
dragging/resizing panels</strong> (<a
href="vscode-webview://0vkpvjgb486eanj04qjfqcnqmnatevjacbbbqdp3ecrs4efo5krm/web/src/views/Dashboards/RenderDashboardCharts.vue"
target="_blank" rel="noopener noreferrer" style="color: rgb(77, 170,
252); text-decoration: rgb(77, 170,
252);">RenderDashboardCharts.vue</a>)</p><ul
style="padding-inline-start: 2em; color: rgb(204, 204, 204);
font-family: -apple-system, "system-ui", "Segoe UI",
Roboto, sans-serif; font-size: 13px; font-style: normal;
font-variant-ligatures: normal; font-variant-caps: normal; font-weight:
400; letter-spacing: normal; orphans: 2; text-align: start; text-indent:
0px; text-transform: none; widows: 2; word-spacing: 0px;
-webkit-text-stroke-width: 0px; white-space: normal; background-color:
rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color: initial;"><li
style="unicode-bidi: plaintext;">Added a live alignment grid that
appears only during a drag or resize, drawn from GridStack's real cell
metrics (192 columns × row height).</li><li style="unicode-bidi:
plaintext;">The grid is<span> </span><strong>operation-aware</strong>:
lines snap to the top/left edges
while<span> </span><em>moving</em><span> </span>and the bottom/right
edges while<span> </span><em>resizing</em>, so the edge you're dragging
always lands on a line and every line is reachable (no skipped
positions).</li><li style="unicode-bidi: plaintext;">Panels are made
opaque during interaction (<code style="font-family: monospace; color:
rgb(208, 208, 208); background-color: rgb(60, 60, 60); padding: 2px 4px;
border-radius: 3px; word-break: break-word; font-size:
0.9em;">--color-surface-base</code>) so the grid only shows in the
gutters/empty cells, not through chart or table panels.</li></ul><p
style="white-space: pre-wrap; margin-top: 0.1em; margin-bottom: 0.2em;
unicode-bidi: plaintext; color: rgb(204, 204, 204); font-family:
-apple-system, "system-ui", "Segoe UI", Roboto,
sans-serif; font-size: 13px; font-style: normal; font-variant-ligatures:
normal; font-variant-caps: normal; font-weight: 400; letter-spacing:
normal; orphans: 2; text-align: start; text-indent: 0px; text-transform:
none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;
background-color: rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color:
initial;"><strong>Empty states</strong> (<a
href="vscode-webview://0vkpvjgb486eanj04qjfqcnqmnatevjacbbbqdp3ecrs4efo5krm/web/src/lib/core/EmptyState/presets.ts"
target="_blank" rel="noopener noreferrer" style="color: rgb(77, 170,
252); text-decoration: rgb(77, 170, 252);">presets.ts</a>, <a
href="vscode-webview://0vkpvjgb486eanj04qjfqcnqmnatevjacbbbqdp3ecrs4efo5krm/web/src/components/dashboards/settings/VariableSettings.vue"
target="_blank" rel="noopener noreferrer" style="color: rgb(77, 170,
252); text-decoration: rgb(77, 170, 252);">VariableSettings.vue</a>, <a
href="vscode-webview://0vkpvjgb486eanj04qjfqcnqmnatevjacbbbqdp3ecrs4efo5krm/web/src/views/Dashboards/ScheduledDashboards.vue"
target="_blank" rel="noopener noreferrer" style="color: rgb(77, 170,
252); text-decoration: rgb(77, 170,
252);">ScheduledDashboards.vue</a>)</p><ul style="padding-inline-start:
2em; color: rgb(204, 204, 204); font-family: -apple-system,
"system-ui", "Segoe UI", Roboto, sans-serif;
font-size: 13px; font-style: normal; font-variant-ligatures: normal;
font-variant-caps: normal; font-weight: 400; letter-spacing: normal;
orphans: 2; text-align: start; text-indent: 0px; text-transform: none;
widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;
white-space: normal; background-color: rgb(24, 24, 24);
text-decoration-thickness: initial; text-decoration-style: initial;
text-decoration-color: initial;"><li style="unicode-bidi:
plaintext;">Replaced the generic<span> </span><code style="font-family:
monospace; color: rgb(208, 208, 208); background-color: rgb(60, 60, 60);
padding: 2px 4px; border-radius: 3px; word-break: break-word; font-size:
0.9em;">NoData</code><span> </span>placeholder with<span> </span><code
style="font-family: monospace; color: rgb(208, 208, 208);
background-color: rgb(60, 60, 60); padding: 2px 4px; border-radius: 3px;
word-break: break-word; font-size:
0.9em;">OEmptyState</code><span> </span>for the Variables settings table
and the Scheduled Dashboards list.</li><li style="unicode-bidi:
plaintext;">Added a new<span> </span><code style="font-family:
monospace; color: rgb(208, 208, 208); background-color: rgb(60, 60, 60);
padding: 2px 4px; border-radius: 3px; word-break: break-word; font-size:
0.9em;">no-variables</code><span> </span>preset (illustration + copy +
"Add variable" action); reused the existing<span> </span><code
style="font-family: monospace; color: rgb(208, 208, 208);
background-color: rgb(60, 60, 60); padding: 2px 4px; border-radius: 3px;
word-break: break-word; font-size:
0.9em;">no-reports</code><span> </span>preset for scheduled
dashboards.</li><li style="unicode-bidi: plaintext;">Fixed the Variables
table so a long
list<span> </span><strong>scrolls</strong><span> </span>instead of
clipping, and made the Scheduled Dashboards table fill the drawer height
so the empty state centers.</li></ul><p style="white-space: pre-wrap;
margin-top: 0.1em; margin-bottom: 0.2em; unicode-bidi: plaintext; color:
rgb(204, 204, 204); font-family: -apple-system, "system-ui",
"Segoe UI", Roboto, sans-serif; font-size: 13px; font-style:
normal; font-variant-ligatures: normal; font-variant-caps: normal;
font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start;
text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px;
-webkit-text-stroke-width: 0px; background-color: rgb(24, 24, 24);
text-decoration-thickness: initial; text-decoration-style: initial;
text-decoration-color: initial;"><strong>Dashboard settings
layout</strong> (<a
href="vscode-webview://0vkpvjgb486eanj04qjfqcnqmnatevjacbbbqdp3ecrs4efo5krm/web/src/components/dashboards/settings/GeneralSettings.vue"
target="_blank" rel="noopener noreferrer" style="color: rgb(77, 170,
252); text-decoration: rgb(77, 170,
252);">GeneralSettings.vue</a>)</p><ul style="padding-inline-start: 2em;
color: rgb(204, 204, 204); font-family: -apple-system,
"system-ui", "Segoe UI", Roboto, sans-serif;
font-size: 13px; font-style: normal; font-variant-ligatures: normal;
font-variant-caps: normal; font-weight: 400; letter-spacing: normal;
orphans: 2; text-align: start; text-indent: 0px; text-transform: none;
widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;
white-space: normal; background-color: rgb(24, 24, 24);
text-decoration-thickness: initial; text-decoration-style: initial;
text-decoration-color: initial;"><li style="unicode-bidi:
plaintext;">Reworked the General settings layout.</li></ul><p
style="white-space: pre-wrap; margin-top: 0.1em; margin-bottom: 0.2em;
unicode-bidi: plaintext; color: rgb(204, 204, 204); font-family:
-apple-system, "system-ui", "Segoe UI", Roboto,
sans-serif; font-size: 13px; font-style: normal; font-variant-ligatures:
normal; font-variant-caps: normal; font-weight: 400; letter-spacing:
normal; orphans: 2; text-align: start; text-indent: 0px; text-transform:
none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;
background-color: rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color:
initial;"><strong>Tab panel-count badge</strong> (<a
href="vscode-webview://0vkpvjgb486eanj04qjfqcnqmnatevjacbbbqdp3ecrs4efo5krm/web/src/components/dashboards/tabs/TabList.vue"
target="_blank" rel="noopener noreferrer" style="color: rgb(77, 170,
252); text-decoration: rgb(77, 170, 252);">TabList.vue</a>)</p><ul
style="padding-inline-start: 2em; color: rgb(204, 204, 204);
font-family: -apple-system, "system-ui", "Segoe UI",
Roboto, sans-serif; font-size: 13px; font-style: normal;
font-variant-ligatures: normal; font-variant-caps: normal; font-weight:
400; letter-spacing: normal; orphans: 2; text-align: start; text-indent:
0px; text-transform: none; widows: 2; word-spacing: 0px;
-webkit-text-stroke-width: 0px; white-space: normal; background-color:
rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color: initial;"><li
style="unicode-bidi: plaintext;">Each dashboard tab now shows a badge
with its panel count (section headers excluded), so tab density is
visible without opening each tab.</li></ul><p style="white-space:
pre-wrap; margin-top: 0.1em; margin-bottom: 0.2em; unicode-bidi:
plaintext; color: rgb(204, 204, 204); font-family: -apple-system,
"system-ui", "Segoe UI", Roboto, sans-serif;
font-size: 13px; font-style: normal; font-variant-ligatures: normal;
font-variant-caps: normal; font-weight: 400; letter-spacing: normal;
orphans: 2; text-align: start; text-indent: 0px; text-transform: none;
widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;
background-color: rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color:
initial;"><strong>Variable dependency indicator</strong> (<a
href="vscode-webview://0vkpvjgb486eanj04qjfqcnqmnatevjacbbbqdp3ecrs4efo5krm/web/src/components/dashboards/settings/VariableSettings.vue"
target="_blank" rel="noopener noreferrer" style="color: rgb(77, 170,
252); text-decoration: rgb(77, 170,
252);">VariableSettings.vue</a>)</p><ul style="padding-inline-start:
2em; color: rgb(204, 204, 204); font-family: -apple-system,
"system-ui", "Segoe UI", Roboto, sans-serif;
font-size: 13px; font-style: normal; font-variant-ligatures: normal;
font-variant-caps: normal; font-weight: 400; letter-spacing: normal;
orphans: 2; text-align: start; text-indent: 0px; text-transform: none;
widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;
white-space: normal; background-color: rgb(24, 24, 24);
text-decoration-thickness: initial; text-decoration-style: initial;
text-decoration-color: initial;"><li style="unicode-bidi:
plaintext;">Variables that depend on others show a "Depends on: …" chip
(tree icon + count) in the settings table.</li></ul><p
style="white-space: pre-wrap; margin-top: 0.1em; margin-bottom: 0.2em;
unicode-bidi: plaintext; color: rgb(204, 204, 204); font-family:
-apple-system, "system-ui", "Segoe UI", Roboto,
sans-serif; font-size: 13px; font-style: normal; font-variant-ligatures:
normal; font-variant-caps: normal; font-weight: 400; letter-spacing:
normal; orphans: 2; text-align: start; text-indent: 0px; text-transform:
none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;
background-color: rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color:
initial;"><strong>Panel header token</strong> (<a
href="vscode-webview://0vkpvjgb486eanj04qjfqcnqmnatevjacbbbqdp3ecrs4efo5krm/web/src/lib/styles/tokens/component.css"
target="_blank" rel="noopener noreferrer" style="color: rgb(77, 170,
252); text-decoration: rgb(77, 170, 252);">component.css</a>)</p><ul
style="padding-inline-start: 2em; color: rgb(204, 204, 204);
font-family: -apple-system, "system-ui", "Segoe UI",
Roboto, sans-serif; font-size: 13px; font-style: normal;
font-variant-ligatures: normal; font-variant-caps: normal; font-weight:
400; letter-spacing: normal; orphans: 2; text-align: start; text-indent:
0px; text-transform: none; widows: 2; word-spacing: 0px;
-webkit-text-stroke-width: 0px; white-space: normal; background-color:
rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color: initial;"><li
style="unicode-bidi: plaintext;">Lightened<span> </span><code
style="font-family: monospace; color: rgb(208, 208, 208);
background-color: rgb(60, 60, 60); padding: 2px 4px; border-radius: 3px;
word-break: break-word; font-size:
0.9em;">--color-panel-bar-bg</code><span> </span>(grey-200 → grey-100)
for a softer chart-header band; the grey-300 divider still carries the
header/body edge.</li></ul><h3 style="unicode-bidi: plaintext; color:
rgb(204, 204, 204); font-family: -apple-system, "system-ui",
"Segoe UI", Roboto, sans-serif; font-style: normal;
font-variant-ligatures: normal; font-variant-caps: normal;
letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px;
text-transform: none; widows: 2; word-spacing: 0px;
-webkit-text-stroke-width: 0px; white-space: normal; background-color:
rgb(24, 24, 24); text-decoration-thickness: initial;
text-decoration-style: initial; text-decoration-color: initial;">
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||
|
|
d770d0245a |
fix: preserve online evaluation provider API keys (#13739)
## Summary - preserve the stored provider API key when an edit submits a blank value - replace the stored key when a nonblank value is supplied - show the create-time required marker only for providers that require authentication - retain keyless creation for self-hosted providers ## Testing - ProviderFormPage Vitest suite passed: 8 tests - Targeted ESLint passed - cargo fmt check passed - git diff check passed - Focused Rust test compilation is blocked by unrelated existing alert-state model errors Related to #13655. Companion enterprise PR: https://github.com/openobserve/o2-enterprise/pull/2390 |
||
|
|
b9e11bb885 |
fix(web): classify unquoted LIKE patterns wherever the parser stops (#13818)
`x LIKE E00%` parses as the identifier `E00` followed by the modulo operator, so the PEG parser fails on a later token and the existing `found === "%"` check never fires — the error fell through to the allow-list suppression and the editor showed no diagnostic at all. Only patterns whose first character terminates the identifier (`LIKE BIN-%`) were caught. Add hasUnquotedLikePattern(), which inspects the LIKE/ILIKE/RLIKE operand nearest the error position and reports badLikePattern when it is a bare wildcard token. It runs immediately before the suppression return, so it can only reclassify errors that were previously dropped; quoted patterns, parenthesised expressions and function calls never match. Detection on the unquote_like_pattern mutation set goes 6/13 -> 13/13, so raise that threshold from 0.5 to 0.9. The old 0.5 sat exactly on the sample boundary, which is why adding one query to the shared query-agent corpus in #13808 turned the suite red. Also stop the per-mutation tests from regenerating the whole mutation pool (~1,000 queries x 16 mutations, each re-parsed) on every case: memoize generateBrokenQueries() and give the heavy tests an explicit 30s timeout. Spec runtime drops from ~112s to ~48s and the 5s-default timeouts go away. |
||
|
|
ddb67f3efb |
fix(synthetics): record from a specific step, and stop silently downgrading recorded actions (#13799)
Supersedes #13777, which carried the record-from-step commits alone. Closing that in favour of this, because the two changes land in the same three files (`types/synthetics.ts`, `useSyntheticsRecorder.ts`, `JourneySteps.vue`) and reviewing them apart means resolving the same conflict twice. ## Problem **A recorded hover was stored as a click.** Playwright 1.56 added `hover` to the recorder's action model, reachable from its "Choose action" picker. The extension's action mapper seeds every step with `action: 'click'` as a placeholder for its switch to overwrite, and returned that placeholder from `default:` — so any action name the switch did not know became a click the author never performed. On the usual hover target — a nav trigger, a dropdown opener — clicking it can navigate or submit. The run still reports green. There are three separate guards meant to stop exactly this, and none of them fired, because the relabel happens upstream of all of them: | Guard | Why it missed | |---|---| | `UNSUPPORTED_REPLAY_ACTIONS` (extension) | step already said `click` | | `RETIRED_ACTIONS` → `isStorableAction` | step already said `click` | | `V2_STEP_ACTIONS` (Rust validation) | `click` is valid | The same picker offers **Right click** and **Double click** as explicit choices. Both were flattened too: `click_count` was dropped by the mapper and never existed downstream, and the probe's executor called `locator.click({ timeout })` while reading neither `button` nor `click_count`. A deliberately recorded right click replayed as a left click; a double click as a single one. **Recording could only start from scratch.** Fixing step 12 of a 15-step journey meant re-recording all 15. ## Change **Record from a specific step.** Each step gets a record action that replays the prefix before it, then flips the recorder into insert mode at that point. A restore that cannot reach the requested step reports where it stopped (`prefixFailed`) rather than silently recording from the wrong place. **Every action Playwright can record is now stored and replayed as itself.** - `hover` is a first-class v2 action — mapped, stored, validated, and executed by `Locator.hover` in the probe and `Frame.hover` in the extension player. - `button` and `click_count` travel to storage, so right click and double click replay as recorded. - An action name the mapper does not know now **throws** instead of returning the `click` placeholder. This is the part that is not hover-specific: upstream added one action in nine minors and will add more, and the next one must be a deliberate decision rather than a silent downgrade. **The extension handshake reports capabilities, not just presence.** `getStatus` now returns `extVersion` and a `capabilities` array; O2 gates affordances on `hasCapability()` with a defined pre-handshake fallback, and warns when the installed build is older than the minimum. Previously the protocol carried no version at all — Chrome updates the extension independently of when O2 deploys, so the two could disagree about the wire with no way to say so. **Two dead message declarations, opposite problems.** `setSources` no longer exists in playwright-core (sources arrive alongside the actions), so its declared variant could never fire — while its `generatedCode` field is exactly what an eject-to-code feature would reach for. Deleted. `elementPicked` is the reverse: it fires on every "Pick locator" and every click in inspecting mode, and was being discarded, which made inspect mode a dead end. Now kept. ### Compatibility `button` and `click_count` are optional with defined absent-behaviour — absent means a single left click. Every journey stored today stays valid, means the same thing, and serialises byte-identically. **No schema version bump and no migration.** A test pins that rather than leaving it as an intention. `hover` is a new vocabulary entry with no default, so the probe must be able to execute one before O2 is allowed to store one — the probe change ships first. ## Implementation | File | | |---|---| | `src/config/src/meta/synthetics.rs` | `hover` joins `V2_STEP_ACTIONS`/`V2_ELEMENT_ACTIONS`; `button` and `click_count` added to `BrowserStepV2` with validation (`left\|middle\|right`, `1..=3`) | | `web/src/types/synthetics.ts` | `button`/`clickCount` on both step types; `extVersion` + `capabilities` on `RecorderStatus`; `setSources` variant deleted and `elementPicked` typed; `prefixFailed` and insert-mode fields on `recordingStarted` | | `web/src/utils/synthetics/mapRecordedStep.ts` | promotes `button`/`clickCount` from the wire, like every other stored field | | `web/src/utils/synthetics/buildV2Steps.ts` | `hover` in `ACTION_TO_V2`; crosses camelCase → snake_case for the two click fields | | `web/src/constants/synthetics.ts` | `hover` out of `RETIRED_ACTIONS`; `MIN_EXTENSION_VERSION` | | `web/src/composables/useSyntheticsRecorder.ts` | `startRecordingFrom`, `hasCapability`, `pickedSelector`, `extensionOutdated` | | `web/src/components/synthetics/journey/{BrowserJourney,JourneySteps}.vue` | per-step record action and its wiring | `click_count` is capped at 3: the recorder only ever emits 1 or 2, and an unbounded count is a way to hold a page's event loop open. The two click fields cross from camelCase to snake_case in `buildV2Step` and nowhere else — reading `step.wire` directly inside it would have worked and invented a second path into storage. ### Depends on a recorder extension build `hover`, the click fields and the capability handshake all need the matching extension build. O2 degrades honestly without it: `hasCapability()` falls back to the pre-handshake set, and an older build raises the outdated-extension warning instead of failing on a message shape it does not recognise. ## Tests - **Rust** — hover accepted as an element action; `button`/`click_count` validated at both bounds; a step carrying neither still validates (the compatibility guarantee). Two existing tests encoded "hover is rejected" and now say the opposite. 101 pass. - **Probe** — hover hovers rather than clicks; a right click sends the recorded button; a double click sends two clicks; a journey stored before these fields replays unchanged. The action union is asserted against the Rust vocabulary, so the next divergence fails a test instead of reaching an executor with no case for it. Also pinned: a selector carrying a frame hop resolves into the iframe, which had no coverage anywhere. - **Web** — 1213 across 60 files. Nine existing tests encoded "hover is retired" across constants, `buildV2Steps`, `buildPayload` and the save-gate schema; each now states the opposite deliberately. - **Extension** — the unmapped-action throw, hover round-tripping both directions, and a double click surviving the round trip as two clicks. `vue-tsc`, `cargo clippy -D warnings`, and `prettier --check` are clean. The last commit is formatting only: the three record-from-step files arrived failing `format:check`, which is a gate of its own and would not have been caught by lint. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
192f11f7ea |
fix(i18n): correct mistranslations in zh-CN locale (#13835)
## Summary
Fixes machine-translation errors in the Simplified Chinese locale
(`web/src/locales/languages/zh-CN.json`), found by sweeping the file
against the English source. Three batches of the same class of error:
### Nonsense translations (wrong sense of the English word)
| Before | After | English source |
|---|---|---|
| 痕迹 / 踪迹 (physical traces) | 追踪 | Traces (nav, tabs, buttons — 8 keys)
|
| 朗姆酒 (the liquor) | RUM | RUM |
| 漂亮 / 崩溃了 (beautiful / crashed) | 格式化 / 折叠 | Pretty / Collapsed view
toggle |
| 汽车 (a car) | 自动 | Auto (layout mode) |
| 转 / 转弯 (road turns) | 回合 | Turn / Turns (conversation) |
| {n} 个电话 (phone calls) | {n} 次调用 | {n} calls (tool calls) |
| oTel 收藏家/代理人 (art collector) | OTel Collector / 代理 | OTel Collector /
Agent |
| 空缺评测任务 (job vacancy) | 打开评估任务 | Open Eval Jobs |
| 简历 (a CV) | 恢复 | Resume (pause/resume) |
| 出口 (trade export, ×3) | 导出 | Export |
| 逃脱了 (ran away) | 已转义 | Escaped (string escaping) |
| 测试赛 (sports match) | 测试匹配 | Test Match |
| 比分 (sports score) | 分数 | scores (eval scores) |
| 好吧 ("well, fine") | 正常 | Ok (status) |
| 领域 (domain/field) | 区域 | Region (bucket region) |
| 全球 (worldwide, ×4) | 全局 | Global (scope) |
| 商店 (a shop) | 存储 | Store (key store) |
| 尺寸 (physical size, ×5) | 维度 | Dimensions (metrics) |
| 用法 / 使用密钥 (how-to / secret key) | 用量 / 用量键 | Usage / Usage key |
| 滤镜 (photo filters) | 筛选条件 | Filters |
| 网络缓存影响 (network) | 缓存净影响 | Net Cache Impact |
| 申请并搜索 (apply for) | 应用并搜索 | Apply & Search |
| 压缩后的尺寸 | 压缩后大小 | Compressed Size |
| 等得紧,我们正在搜寻你的踪迹。 | 正在加载追踪... | Loading Traces... |
### Terminology unification
- **GenAI agents → 智能体** (agent graph, agent behavior, agent mapping,
sessions — ~40 strings). Infrastructure agents (host collectors,
synthetics runners) keep the conventional 代理.
- **Eval jobs → 评估任务** (was a mix of 评估作业 / 评测任务 / 作业). Batch jobs
elsewhere (backfill, search jobs) keep 作业.
- **Enrichment table → 富化表** (was a mix of 充实表 / 丰富表 / 扩充表).
- **Cipher Key → 加密密钥** (was mixed with 密码密钥); **Actions → 操作** (was 行动
in 4 places); **Running Queries → 运行中的查询**; synthetics **journey → 流程**
(was 旅程); assistant 助理 → 助手.
- `shortcuts.actions.tracesSearch` resynced with current English source
("Run query" → 运行查询).
### Cosmetic
- Shortcuts cheatsheet: corrupted ⌘ symbol (rendered as ▼) restored;
ungrammatical strings fixed (按?任何地方 → 在任意位置按 ?, 焦点搜索栏 → 聚焦搜索栏, 导入控制面板 →
导入仪表板); untranslated example values restored to `input`/`output`;
half-translated strings completed (Spans 表格视图 → 跨度表格视图, Stacktrace 已复制 →
堆栈跟踪已复制).
Notes:
- zh-TW.json was checked for the same error patterns and is clean.
- 跟踪 vs 追踪 for "trace" is still mixed in a few longer sentences; left
as-is since 跟踪 is not incorrect, only inconsistent.
## Test plan
- JSON validated with `python3 -c 'json.load(...)'` after each batch;
keys unchanged, only values edited (~200 lines).
- Grep confirms none of the wrong terms above remain in zh-CN.json.
|
||
|
|
7f74925605 | feat: banner as notification (#13667) | ||
|
|
c246119809 |
fix(i18n): stop translated placeholder names crashing localized pages (#13833)
Fixes #13751 ## The crash vue-i18n compiles messages just-in-time (`__INTLIFY_JIT_COMPILATION__`), and a compile error is **thrown**, not warned — it tears down whatever component asked for the message. The translation pipeline had been localizing placeholder *names*, which is exactly that kind of error: ``` en-US "Are you sure you want to delete {identifier}?" zh-CN "您确定要删除 {标识符} 吗?" -> SyntaxError: 2 (INVALID_TOKEN_IN_PLACEHOLDER) ``` IAM → Service Accounts rendered blank in Chinese for this reason. The two zh-CN strings named in the issue had already been patched by hand in #13707, but the *class* was never swept. ## What was actually broken Compiling every message in every locale file with vue-i18n's own compiler found three groups: | | count | symptom | |---|---|---| | Does not compile | 132 | page blanks with `SyntaxError` — 8 locales: es, fr, it, ja, ko, nl, pt, tr | | Compiles, unknown name | 147 | `{Anzahl}` for `{count}` → value renders as an empty string | | Stale text, dropped placeholder | 5 | `{org}`, `{payer}`, `{endpoint}`, `{service}` — English source no longer has them | Worst of the first group are mangled vue-i18n literal escapes, e.g. `{'{'}team{'}'}` coming back as `{'{'} '}` — an unbalanced brace. All 279 are corrected here. The existing translated prose is kept verbatim; only the tokens are restored. The 5 stale ones are retranslated from the current English text, since restoring a token that no longer exists is not an option. ## Why it can't come back **`web/src/locales/enUsMessages.spec.ts` already did this — for en-US only.** That left the machine-translated locales, the actual source of the bug, uncovered. Renamed to `localeMessages.spec.ts` and extended to every locale, plus a second assertion that a translation only interpolates names en-US defines. That second check is a **subset** rule, not equality: dropping a placeholder the target language does not need stays legal (an English plural suffix like `{plural}` has no Japanese equivalent). Introducing a name en-US never passes a value for is always a bug. Placeholder names are collected by *running* the compiled message against a recording context rather than by pattern-matching the source, because only the compiler knows which braces are placeholders and which are literal escapes — `{'}'}` is one token whose body is the character a `{...}` regex stops at. All plural branches are evaluated. **`scripts/translations/translator.py` — `_validate` had the same regex blind spot** (`{[^{}]*}`) and never checked that the output compiles at all, which is why these landed. It now tokenizes the way vue-i18n does and rejects anything vue-i18n would refuse, so the pipeline stops emitting them rather than CI merely catching them afterwards. Escape drift (`{'{'} rows {'} rows {'}`) is caught too, because literal escapes are now part of the compared token multiset. ## Verification - The Python scanner was cross-checked against vue-i18n's compiler over **all 170,055 messages** in the 15 locale files — identical compile verdicts and identical token sets, 0 divergences. - Injecting the original `{标识符}` string back into zh-CN fails the new spec; injecting a compiling-but-wrong `{bilv}` fails the second assertion. - `npx vitest run --environment jsdom --root src/ locales/` → 45 passed. `prettier --check` and `tsc` clean on the spec. - Only `en-US.json` is imported anywhere in `web/src`, so no existing test depends on the changed locale data. ## Not in scope A handful of translations are awkward but compile and bind correctly (e.g. ko-KR renders "ingestion limit" with the eating sense of the word). Left alone to keep this diff auditable as a mechanical token restoration. |
||
|
|
0dba67d638 |
feat: composite alerts (#13797)
Design at : alerts/composite_alerts/composite_alerts.md ## Summary - add composite alert graph persistence, evaluation, scheduling, lifecycle APIs, and deletion safeguards - add composite alert UI forms, preview, references, list/detail integration, and localization - add Rust, API, Vue, and Playwright contracts ## Verification - cargo fmt --all -- --check - cargo check -p openobserve-core - cargo check -p openobserve-api-management - npm run type-check:composite-alerts - npm run type-check:app - npm run lint:ci -- --quiet - design/token/style checks - targeted Rust/Vitest suites - browser QA against local alerts UI ## Follow-ups (intentionally deferred, documented in the spec) - `RunOutcome::Error`/`NotifyFailed` outcome records on composite delivery failure (§11.5) - Incident detail live-definition resolver for composites + "definition unavailable" retention (§9.6) - `alert_composite_*` observability metrics (§16) - SLO/stream delete-conflict 409 bodies with readable-vs-hidden parent split (§10.3) - Super-cluster startup preflight (§18) <img width="1650" height="979" alt="image" src="https://github.com/user-attachments/assets/3def5246-7bf7-48fe-9a42-c0ac08271ea9" /> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
3fda083f51 |
feat(traces): add trace time index (#13827)
Design at: [https://github.com/openobserve/designs/blob/main/infrastructure/file-list/trace-time-index.md](https://github.com/openobserve/designs/blob/main/infrastructure/file-list/trace-time-index.md) ## Summary - add an hourly Metadata time index per trace stream to resolve a trace ID to its full time range - add external time-range and trace-details APIs, including bounded locate/expand behavior and standard search responses - integrate index generation into trace ingestion and preserve query visibility through the synchronous Metadata write path - synchronize retention and deletion for associated Metadata streams - reuse the shared trace stream permission check across trace APIs ## Why Trace detail lookup currently depends on a caller-provided time range, which can omit spans outside that window. The time index provides a bounded, best-effort range lookup for `stream + trace_id` without scanning the full trace stream. Closes #13823 ## Environment variables | Variable | Default | Effect | | --- | --- | --- | | `ZO_TRACE_TIME_INDEX_ENABLED` | `true` | Enables per-trace-stream Metadata indexes, index writes during trace ingestion, and indexed time-range lookup. Only traces ingested after the feature is enabled are indexed; there is no historical backfill. | | `ZO_QUERY_TIMEOUT` | 600 seconds | Existing setting reused as the cumulative Trace Timer locate/expand deadline and as the default timeout for the trace-details search. | When enabled, each traces stream can create a related Metadata stream named `trace_time_index_{stream}`. Its retention settings follow the source stream, and it is removed with the source stream. ## API changes All timestamp parameters and returned time ranges use microseconds. ### Get a trace time range ```http GET /api/{org_id}/{stream_name}/traces/{trace_id}/time_range ``` Query parameters: - `hint_ts` (optional): timestamp hint used to start the index lookup Returns the source stream, trace ID, and the resolved `start_time` / `end_time`. The `range` field is `null` when the index cannot resolve the trace. ### Get trace details ```http GET /api/{org_id}/{stream_name}/traces/{trace_id}/details ``` Query parameters: - `start_time` and `end_time` (optional, but must be supplied together): caller-provided range - `hint_ts` (optional): timestamp hint for the time-index lookup - `timeout` (optional): trace-details search timeout in seconds The endpoint unions the caller range with the indexed range, so it never narrows a caller-provided range. If neither the caller range nor the index can provide a range, it returns `400`. The response uses the standard search response and includes the effective range in `new_start_time` and `new_end_time`. ## Validation - `cargo test -p openobserve-api-search --lib` — 145 passed - `cargo test -p openobserve-core traces::time_index::tests --lib` — 7 passed - `cargo test -p infra db::trace_time_index::tests::test_coverage_start_cache -- --exact --nocapture` — passed - `cargo check -p openobserve-api-search` - `cd web && npm run type-check:app` - end-to-end ingestion and query validation with the data generator ## Validation notes - targeted ESLint could not start because the local web dependencies are missing `@eslint/css` - enterprise-only compilation could not be completed from this OSS checkout because the local enterprise overlay is unavailable |
||
|
|
dbee572722 |
fix: slo bugs (#13784)
closes https://github.com/openobserve/o2-enterprise/issues/2397 --------- Signed-off-by: Yashodhan Joshi <yjdoc2@gmail.com> Co-authored-by: Yashodhan Joshi <yjdoc2@gmail.com> Co-authored-by: sai nikhil kethe <nikhil@openobserve.ai> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
6927c2f335 |
fix(synthetics): collapse the variables panel by default (#13825)
## What The variables panel on the browser-test **Journey** step now starts collapsed. ## Why The journey is what that page is for, and the panel was taking a share of the width before the author had asked for it. The toolbar toggle sits right next to the journey and is labelled, so bringing the panel in is one click — and the splitter position is still shared with Configure, so nothing about the panel's own behaviour changes. ## Scope One line plus a comment. `variablesPanelOpen` is journey-only by design (the existing comment above it explains why it is not shared with Configure), so this cannot affect the Configure step. ```diff -const variablesPanelOpen = ref(true); +const variablesPanelOpen = ref(false); ``` ## Testing `npx vitest run src/views/synthetics/CreateBrowserTest.spec.ts` — 44 passing. Split out of a larger synthetics branch so it can land on its own. |
||
|
|
0ed5336bf1 | chore: adding HA mode for o2-ai agent (#13525) | ||
|
|
e490a5c347 |
fix(logs): load the stream list on Visualize when no stream is selected (#13812)
Entering Visualize from the logs page with no stream selected left the
Stream dropdown on "No options found" — enabled, openable, and empty, so
there was no way to pick one.
PanelFieldList defers its initial fetch in edit mode until a stream is
set, exempting only "metrics". The Visualize tab ("build") needs the
same exemption: its select is the user's own, and the list is keyed on
stream_type alone, so waiting for a stream buys nothing.
Also arms the stream_type watcher at mount, so changing Stream Type with
a blank stream now reloads the list instead of doing nothing.
- https://github.com/openobserve/o2-enterprise/issues/2165
|
||
|
|
d1f8941512 | test: stabilize test cases for the Alpha environment (#13551) | ||
|
|
e4503f7fcb | fix: Fix/match all pipe query corruption (#13764) | ||
|
|
3d06ed0423 |
fix(logs): reject LIMIT in filter mode instead of building an invalid query (#13773)
Closes #6915. Without SQL mode the search bar's contents become the WHERE body of a generated `select [FIELD_LIST][QUERY_FUNCTIONS] from "[INDEX_NAME]" [WHERE_CLAUSE]`, and `handleNonSqlMode` splices the input in verbatim after stripping comments and quoting known field names. Nothing checks that the input is actually a predicate, so `k8s_namespace_name = 'nginx-ingress' LIMIT 10` produces valid SQL. The results grid runs it and returns rows, while the histogram query built from the same input is rejected by `is_eligible_for_histogram` and surfaces `SearchHistogramNotAvailable`. That is the split described in the issue — the log list works, the histogram errors. The server side of this already exists, so the guard belongs in the UI, before either request goes out. ## What changed | | | |---|---| | `utils/query/nonSqlLimit.ts` | new — `hasLimitClause()` | | `useSearchQuery.ts` | the guard in `handleNonSqlMode`, using the file's existing `notificationMsg` + `return null` path | Placement note: `hasLimitClause` lives in `utils/query/` rather than in the composable because it holds no state and touches no reactivity — the same split `doubleQuoteWarnings.ts` and `colorizeQuery.ts` already use. The check is deliberately narrow. `LIMIT` matches only when followed by a number, so `limit = 5` and `rate_limit >= 100` pass through. String literals are blanked first, so `msg = 'rate limit 500 exceeded'` is unaffected. Line and block comments are stripped after that, so `code = 200 -- limit 10` — which works today — keeps working. ## Tests - **17** on the derivation — the issue's own query, case variants, whitespace and newlines before the number, a bare `LIMIT 100`, a field named `limit`, `rate_limit >= 100`, the word inside a literal, a literal with a doubled quote, `limit = 'none'`, leading / trailing / block comments, a real LIMIT alongside a comment, and a dash sequence inside a literal. - **6** on `buildSearch` — rejects the reported query and sets the message, still builds without a LIMIT, allows `limit = 5`, ignores commented-out LIMITs, and does not fire in SQL mode. Each rejection test was checked to fail when the guard is reverted. The negative cases stay green either way, which is what separates them from tests that merely restate the implementation. ## Verification - `npx vitest run --root src/ composables/useLogs utils/query` — **1965 passed**, 33 files, 0 failed - `npx vue-tsc --noEmit -p tsconfig.vitest.json` — clean, and identical to `main` - `npx eslint` on all four files — clean - `npx prettier --check` — clean ## Caveats - Not exercised in a running app; covered by unit tests only. - `ORDER BY`, `GROUP BY` and `HAVING` reach the same splice and are **not** handled here. Happy to extend if you want them, but the issue asked about `LIMIT` and I didn't want to widen it uninvited. - `plugins/logs/IndexList.vue` holds an independent copy of the same `[WHERE_CLAUSE]` splice for field-value lookups. It is reached by clicking a field rather than running a search, so it sits outside this path — flagging it rather than changing it. --------- Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com> |
||
|
|
fb50aaea7f |
fix(deps): resolve 39 Dependabot alerts (#13774)
## Summary - update vulnerable web transitive dependencies (`axios`, `brace-expansion`, `fast-uri`, `http-proxy-middleware`, `js-yaml`, `uuid`, and `websocket-driver`) - remove the unused `node-polyfill-webpack-plugin`, eliminating the unpatched `elliptic` dependency chain - update Playwright to 1.55.1 and `node-forge` to 1.4.0 in the UI test lockfile - adapt `BaseImport` to Axios 1.18's stricter response-header type This is expected to resolve 39 of the 41 currently open Dependabot alerts. ## Remaining alerts - `rmcp` remains on the project's custom `rmcp-openapi` fork. Updating it safely requires a coordinated change to the enterprise `Cargo.toml.openobserve`, which currently replaces the OSS manifest during enterprise builds and pins the old API. The compatible intermediate upstream releases also lack the fork's required `allOf`/`anyOf` support. - `thrift` 0.17.0 remains through `parquet` 58.4.0. The patched Thrift line (0.23+) is outside Parquet 58's version constraint. Parquet 59 removes the Thrift dependency, but adopting it requires a coordinated Arrow/DataFusion 59 upgrade. ## Validation - `npm ci` and `npm audit --package-lock-only` in `web/` (0 vulnerabilities) - `npm ci` and `npm audit --package-lock-only` in `tests/ui-testing/` (0 vulnerabilities) - `npm run type-check:app` - `npm run lint:ci -- --quiet` - `npm run lint:design:strict` - `NODE_OPTIONS=--max-old-space-size=8192 npm run build` - `npm run test:unit -- --run` (41,917 passed, 404 skipped) - `npx vitest run src/components/common/BaseImport.spec.ts --environment jsdom` (41 passed) - Playwright test discovery under 1.55.1 (2,076 tests in 265 files) - `cargo fmt --all -- --check` - `cargo test -p openobserve-mcp` (94 passed, 1 ignored) - `cargo audit` (0 vulnerabilities; 2 allowed unmaintained warnings) |
||
|
|
360b989272 |
feat(slos): PromQL support for count and time-sliced SLOs, plus SLO/alert regression fixes (#13761)
Supersedes #13738, whose commit is included here as the first of four. Adds PromQL support to count and time-sliced SLOs, lets metrics streams choose SQL or PromQL, and fixes three shipped bugs found along the way. ## Shipped bug: every SQL time-slice SLO recorded wrong history `fetch_rows`' `SliQueryPlan::Single` arm normalized every hit with `with_good_column: true`, but that arm serves **two** shapes and only one projects `zo_slo_good`: | shape | projects | result | |---|---|---| | count single-query | `zo_slo_good` + `zo_slo_value` | correct | | time slice | `zo_slo_value` only | `good` defaulted to `0.0` | So every SQL time-slice slice was classified from `0.0` instead of its aggregate. With a positive threshold, `<`/`<=` (e.g. `p95 < 300ms`) recorded every slice **good** — the SLO read ~100%, the budget never burned, and no alert on it could fire. `>`/`>=` recorded every slice **bad** and fired continuously. Shipped in v0.92.0 (`4772df1723`); `to_row` had no time-slice test, which is how it got through. Fixed by deriving the flag from the SLI type in a new `single_query_rows`. Deliberately **not** fixed inside `to_row` by falling back to `total` — that would read a count bucket whose `SUM` came back NULL as *fully good*, inventing uptime. A test forbids it. **Stored history is not rewritten.** It ages out within one window; an operator can force a rebuild sooner by making a real edit (a no-op re-save does nothing — `update` bumps only on `definition_changed`). No auto-migration: bumping every time-slice SLO at boot would freeze every attached alert org-wide until backfill cleared the coverage floor, and double each org's row budget for 97 days — a self-inflicted monitoring gap to repair a reporting error. ## PromQL support **Time slice** carried a `query_language` field the planner ignored, so a PromQL time-slice SLO saved cleanly and measured nothing, forever. - `SliQueryPlan::PromQlValue` — one expression, not a good/total pair, planned at slice ends from the same helper the count arm uses. - The expression is passed through **verbatim**. Grouping comes from series labels; wrapping it in `sum by (...)` the way the SQL arm injects GROUP BY would destroy the aggregate (the sum of four pods' p95 is not a p95). - `promql_value_rows` **rejects** two series colliding on one `(slice_start, group_key)` via `RejectReason::AmbiguousSeries` instead of summing. Summing is right for counters (what `promql_rows` does) and meaningless here. Detection is unconditional, so the ungrouped case — an unaggregated `histogram_quantile` returning one series per instance — is covered. Rejects are plumbed through `fetch_rows` into `PassResult.rejected` on both the live and backfill paths. **Count** already had a working `CountSource::PromQl` backend that no UI could reach. ## Metrics streams choose SQL or PromQL `language_suits_stream` refused SQL whenever `stream_type == "metrics"`, and the form derived the language from the stream type — so metrics had no SQL option. A metrics stream is an ordinary stream with a `value` column, and `SELECT histogram(_timestamp, '5 minute'), avg(value) FROM cpu_usage` is a perfectly good SLI. SQL is now valid for any stream type; PromQL stays metrics-only. The form asks via a toggle (defaulting to PromQL) instead of deriving. ## Validation - `scope` refused for PromQL — a SQL `WHERE` fragment with nowhere to go in a PromQL plan. - Empty `query` refused for **both** languages (the SQL aggregate was previously never validated — same silent-freeze shape). - Expressions parsed with `promql-parser` at save, in `core` (`config` has no parser). - **Parse-only by design.** A grouping check requiring the root to be an aggregation whose `by(...)` matches `group_by` wrongly rejects `histogram_quantile(0.95, sum by (le, region)(...))` (function-call root; `le` is a necessary label superset) and binary-op roots like `sum by (region)(increase(a[5m])) - sum by (region)(increase(b[5m]))`. A "warn instead" variant needs a warnings channel the save API doesn't have. ## Also fixed - **Time-slice comparator 422.** `SliConfig::TimeSlice.comparator` has no serde default, so a dropdown the user never opened was omitted and the save died with a bare 422 — blocking creation of *any* time-slice SLO from the UI. Seeded to `<` only when blank, so a stored comparator survives an edit. - **SLO/alert/service-stream regressions** (the commit from #13738): fractional-threshold serde round-trip, `SloAlertCondition` UI, alerts table, service streams. ## Verification - 487 config · 206 core · 45 infra · 418 frontend — all passing; clippy `-D warnings`, `cargo fmt`, prettier, eslint, `vue-tsc` all clean. - Driven through a real browser against a running server (isolated instance, own data dir). All four SLI/language combinations create and store correctly: count·PromQL, count·**SQL over metrics**, time-slice·PromQL, time-slice·**SQL over metrics**. Also verified: the toggle swaps field shapes both ways, scope shows for SQL and hides for PromQL, PromQL-only hints appear only in PromQL mode, and editing a stored SQL-over-metrics SLO reopens as SQL with its predicate intact. - Validation rejections confirmed live with actionable messages: PromQL-with-scope, unparseable PromQL, and an empty SQL aggregate. Known limitation, pre-existing: `threshold` likewise has no serde default and is not seeded, so leaving it blank still 422s. That is genuine user error rather than an untouched-control trap; adding form validation was out of scope. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
b07b3d4229 |
feat(functions): replace language radios with a segmented toggle (#13766)
The Add Function header selected VRL vs JavaScript with a radio group and a single info icon. That icon only ever described the ALREADY-SELECTED language, so learning what the other one was for meant switching to it first. - Radio group -> OFormToggleGroup segmented control with a sliding indicator, each option carrying a language mark (V / JS). The badge variants match the transType column in FunctionList so VRL/JS read the same everywhere. - Give EACH option its own info icon and static tooltip, so both languages are readable without selecting either. Forced-language hosts (workflow function nodes are JS-only) hide the toggle, so the locked language's tip moves to a standalone icon. - Add a pipe divider between the two options, and one between the function-name title and the toggle. data-test renames `...-vrl-radio` -> `...-vrl-option` (same for js): "radio" described the old implementation. E2E page objects would have broken on this change regardless of naming -- toggle items expose data-state="on|off", not the radio group's "checked|unchecked". Verified: FunctionsToolbar.spec.ts 18/18 pass, vue-tsc clean. ESLint could not be run locally (@intlify/eslint-plugin-vue-i18n missing from node_modules). |
||
|
|
4b9204f69f |
feat(synthetics): collapse journey advisories into an on-demand suggestions chip (#13759)
## Problem
The browser journey editor stacked two **always-on** advisory cards
between the toolbar and the first step:
- *"This journey does not verify anything"* — no `assert` step in the
journey
- *"No test attributes found in this recording"* — no step produced a
`test_attribute` locator candidate
Both derived their visibility from the journey's own content, and both
conditions are the normal case for a real recording — so they rendered
on every visit to every affected journey. Each was dismissible, but
dismissal lived in a local `ref(false)`, so it died on remount: authors
closed the same two cards on every visit and still began reading their
steps two cards down the page.
## Change
One warning badge in the Steps toolbar — a warning icon and a count, no
prose:
- **Hover** names it ("1 suggestion for this journey"), from the same
string as the trigger's `aria-label` so the two cannot drift.
- **Click** opens a popover with each suggestion in full, separated by
rules, plus a footer stating that suggestions never block saving or
running.
- **Never opens on its own**, on any render.
- **No dismiss.** The chip costs no vertical space, so suppressing it
buys nothing — and a suggestion that can be waved away is one an author
never has to actually decline. A suggestion leaves when the condition
behind it is resolved.
A zero-assertion journey remains accepted with a warning; nothing here
blocks a save.
Transient banners are deliberately untouched — replay
running/passed/failed/stopped, incognito, pre-flight, recorder error.
They appear because something just happened, and a replay result that
does not announce itself is a worse product.
## Implementation
| File | |
|---|---|
| `web/src/utils/synthetics/journeySuggestions.ts` | new — pure
derivation over the steps; one small producer per rule; carries i18n
**keys** rather than text, so it needs neither Vue nor `useI18n` and a
rule is testable without mounting anything |
| `web/src/components/synthetics/journey/JourneySuggestions.vue` | new —
chip, tooltip, popover; presentation only |
| `web/src/components/synthetics/journey/BrowserJourney.vue` | computes
the suggestions, renders the chip in the toolbar, appends the offered
assertion |
| `ZeroAssertionNotice.vue`, `TestIdMisconfiguredNotice.vue` (+ specs) |
deleted — each was a visibility computation plus a card, and both halves
now have better homes |
Adding a future advisory is one producer and no template change.
Placement note: this lives in `utils/synthetics/` rather than a
composable because it holds no state and touches no reactivity — the
same split `deriveLocatorKind.ts` and `locatorStability.ts` already use
for derivations consumed by these components.
## Tests
- **17** on the derivation — both rules, empty journey, navigate-only
journey, empty candidate list, a test attribute outranked *within* a
step, both conditions at once in stable order, and that the offered
assertion is empty (`element_visible` with no selector/locator) rather
than guessed.
- **15** on the component — no chip at zero, count only and no prose,
pluralization, closed on mount, opens on click, tooltip suppressed while
open, action emits and closes, separators between suggestions.
- **5** on the `BrowserJourney` wiring — old hooks gone, suggestions
handed down, nothing offered when `readonly`, the assertion appended to
the tail with prior step order and ids intact, and the suggestion
dropping once the parent commits.
Two things the tests caught that review would not have:
1. Resolving the last suggestion unmounts the popover but not the
component holding its `open` flag — so a panel left open came *back*
open the next time a suggestion appeared. Regression test written,
confirmed failing, then fixed.
2. One component block runs against the **real** `OPopover` and `OBadge`
instead of a stub. `OBadge` declares `inheritAttrs: false` and the
popover trigger is `as-child` — precisely the pairing that silently
drops a click handler and leaves a chip that looks correct and does
nothing. Every other test stubs the popover and would not see it.
## Accessibility
`⚠ 1` has an accessible name of "1", so the trigger's `aria-label` is
load-bearing and carries the full count string. Child-mode tooltips are
pointer-only, so a keyboard user's path is that label plus the popover
they open with Enter.
Accepted trade-off, stated plainly: the old cards carried
`role="status"` and were announced on appearance. Nothing is announced
now — that announcement is what this change exists to suppress.
## Verification
- `npx vitest run src/components/synthetics src/utils/synthetics
src/views/synthetics` — 1147 passed, 58 files
- `npm run type-check` — clean
- `npm run lint` — clean for every touched file (the repo's pre-existing
`eslint.config.js` / `vitest.config.ts` findings are unchanged and
present on `main`)
|
||
|
|
de0d37e23d |
fix(alerts): stop losing Slack alerts on undeliverable link URLs (#13742) (#13753)
Fixes #13742 Fixes openobserve/o2-enterprise#2394 ## The bug A content template link with a non-http scheme (`javascript:alert(1)`) made Slack reject the **entire** notification with `400 invalid_attachments`. The alert was lost outright and `triggers` recorded `notify_failed`. ## Root cause `safe_url` neutralises a hostile scheme by substituting `BLOCKED_URL` (`"#"`). That is **safe** but not **deliverable** — Slack, Teams, Discord and PagerDuty validate action URLs server-side and reject the whole payload when one is not an absolute `http(s)` URL. A security substitution was turning into total message loss. The two properties are different, and links need both. So the fix splits by medium: | renderer | link handling | |---|---| | Slack buttons, Teams actions, Discord `embed.url`, PagerDuty `href` | `dispatchable_url` → **drop** | | HTML email, email plaintext, SNS plaintext, Discord description | `safe_url` → substitute `#` | | Webhook envelope | unfiltered — versioned machine contract, documented | Dropping beats rewriting: the alert body is worth more than the button, and a button pointing somewhere misleading is worse than no button. `dispatchable_url` requires a **non-empty authority**, not just an allowlisted scheme — `http:javascript:alert(1)` and `https://` are valid *opaque* RFC 3986 http(s) URLs that Slack still rejects. It also rejects raw spaces and control characters, which variable substitution can introduce. ## Related issues fixed in the same area - **Empty `ZO_WEB_URL`** (not in the report): `alert_url` becomes a root-relative `/web/logs?…`, which `safe_url` passes through by design, and it lands in the always-appended "View in OpenObserve" button. Same `invalid_attachments`, on deployments that never authored a hostile template. - **PagerDuty** validates `links[].href` server-side too — same lost-alert class, converted. - **Save-time rejection** (`ContentSpec::validate`): a clear 400 naming the offending link, instead of a template whose link silently never works. Also wired into **preview** and **test-send**, which previously accepted a draft that couldn't be saved and echoed the raw hostile URL back to the UI. - **Slack image-fallback misdiagnosis**: removing the image proves the image was at fault, but only an image pointing at *our* `web_url` justifies tripping the process-wide chart suppression (1h, across every alert) and telling the operator to fix `ZO_WEB_URL`. A custom template's third-party image no longer does either. - **a11y**: `OInput` links its error message to the control via `aria-describedby`; the textarea branch also gained `aria-invalid`. ## o2-enterprise#2394 (fixed here, not in the enterprise repo) Clearing the Monaco body and saving toasted "Please fill required fields" with no indication of *where*. The schema already produced an error on the `body` path — a bare Monaco bridged in via `setFieldValue` just had no `OFormInput` error slot to render it. Now a red border + message driven by the same field state `OFormInput` reads. The aria wiring goes on the **shell**, not on `<QueryEditor>`: `CodeQueryEditor` sets `inheritAttrs: false` and binds `$attrs` to a non-focusable wrapper, so attributes passed to the component would be inert — false assurance for a screen-reader user. > The issue names the title field too. It's an `OFormInput` and already highlights correctly; I added a regression test pinning that rather than changing it. This fix is entirely in OSS (`web/`) since the template form isn't enterprise-gated, so there's no companion enterprise PR. ## Layering note Save-time validation is a **UX gate, not the security boundary**. Link URLs are templates — a scheme that only materialises after `{variable}` substitution can't be judged at save time. The render layer is what actually stops delivery, and it also covers every template saved before this validation existed. Both layers are needed; neither is sufficient. ## Testing - Every fix written test-first (verified RED) and **mutation-tested** — each new test fails when its fix is reverted in substance. - Golden files unchanged: legitimate URLs render byte-identically, so no over-blocking. - Rust: `alerts::notifications` 159 · `config` 3130 · `api-management` 424 — all passing. `cargo clippy --all-targets` and `cargo fmt --check` clean. - Frontend: full suite **41,597 passing** (run because `OInput` is shared library code). `vue-tsc`, `eslint`, and `npm run format:check` clean. - Two independent cold-start review passes; findings from both are folded in, including a P0 where the first version of `dispatchable_url` still let the original bug through. Pre-existing and unrelated: `alerts::destinations::tests::test_alert_destination_template_is_optional` fails locally on a DB connection reset — verified failing on unmodified `main` too. |
||
|
|
883fc97046 |
fix(dashboard): deduplicate panel and layout IDs on import and dashboard load (#13760)
https://github.com/openobserve/openobserve/issues/13104 |
||
|
|
786f4f9f72 |
fix: dashboard label overlap issues (#13725)
1. <img width="727" height="242" alt="image" src="https://github.com/user-attachments/assets/2a84c1bf-2bc0-4fa7-aae5-e7801b99c93b" /> 2. <img width="167" height="380" alt="image" src="https://github.com/user-attachments/assets/6d937583-eb51-458c-8dac-fcf668f1f145" /> 3. <img width="1072" height="150" alt="image" src="https://github.com/user-attachments/assets/4f98ff15-06e4-45d2-a109-c84064cb644b" /> 4. <img width="32" height="289" alt="image" src="https://github.com/user-attachments/assets/9e4ef37f-2e9a-4259-9c6e-041946e1e380" /> 5. <img width="500" height="622" alt="image" src="https://github.com/user-attachments/assets/c4566094-6f58-4692-9ce8-bbcaf61026d7" /> 6. <img width="388" height="644" alt="image" src="https://github.com/user-attachments/assets/03e14424-a59a-4738-b0aa-1f70c7d207f6" /> 7. <img width="388" height="564" alt="image" src="https://github.com/user-attachments/assets/a1acded3-b2d6-4690-b2ba-47397f6a3fb4" /> |
||
|
|
bbd8601c64 |
feat(llm): add annotation queues and datasets (#13719)
## Summary - add annotation queue and dataset storage, management APIs, and review workflows - add discovery, dataset, queue, and review user interfaces - add RBAC synchronization, super-cluster support, tags, version history, and review progress ## Validation - CI should pair this branch with the identically named Enterprise branch for cumulative compilation testing ## Migration note This is an additional cumulative PR created from the committed ud/ebb3 tip. The existing SPR pull requests and branches are intentionally left unchanged. --------- Co-authored-by: sai nikhil kethe <nikhil@openobserve.ai> |
||
|
|
43b531d996 | fix: build warning v2 (#13729) | ||
|
|
b3e6a77270 |
fix(correlation): service-streams P2 behavioral fixes + discovered services flat table (#13710)
Correlation P2 round: behavioral/security fixes across the service-streams backend and correlation FE, plus the Discovered Services UX rework and the correlation e2e suites. **Companion enterprise PR (must merge together):** openobserve/o2-enterprise#2381 — the enterprise side calls the new 3-arg `put(org, record, max_streams_per_type)` introduced here. ## Commit 1 — behavioral fixes (F2–F38) **Backend** - F25/F26: RBAC on `list_services` (`service_streams` GET — the one previously-unenforced endpoint in the module) + validate `distinguish_by` ids on identity-config save - F2: validate trace filter param before WHERE-clause interpolation - F6: `_reset` invalidates caches on all nodes via reload event; org deletion clears service-streams caches - F8: shared coverage-based identity-set ranking for ingest and read - F10: purge rows of removed/changed identity sets on config save - F19: `put` returns deleted orphan keys for cache eviction - F28: discriminate no-match from failure; stop aliasing 404 to no-match - F32: cap DB-side stream union at `max_streams_per_type` - `all_dimensions` unions on put so late-tracked dimensions surface - F14: `StreamInfo.dropped_dimensions` contract field; allow empty `tracked_alias_ids` when identity sets exist **Frontend** - F35/F36/F31: per-stream filter-edit resolution via semantic groups; dimension edits in both key spaces; subject overrides never guess fields - F27/F30: echo `source_stream`/`source_type`; stop guessing another stream's filters; chip dedup - F2/F38: SQL field/value escaping via shared `quoteSqlLiteral` helper ## Commit 2 — Discovered Services UX + e2e - Flat table (one row per instance), same-name services merge their name cell (OTable `pivotRowColumns`); optional per-service collapse with aggregated summary row; search/filters override collapse - Fixes an OTable pivot-merge bug (value-keyed merge map blanked single-column runs) + regression test - Semantic Field Groups: category tabs with counts + cross-category search with match highlighting - Correlation Phase-1 API + Phase-2 UI Playwright suites ## ⚠️ Release note (enterprise + RBAC deployments) Viewing Discovered Services now requires the **Service Streams** permission (previously unenforced). Root/admin users are unaffected; custom roles may need a one-time grant via the role editor (resource already exists in the OFGA model). ## Checks - `cargo fmt --all --check` ✅ · `cargo clippy --all-targets` ✅ enterprise + OSS feature sets (0 warnings) - prettier ✅ · eslint 0 errors · 207 unit tests on touched specs ✅ - Compiles with paired enterprise branch ✅ |
||
|
|
a2f950153e |
fix(logs): pass the expanded row to View Trace (#13722)
#13708 ## Problem Clicking **View Trace** inside an expanded log row on the Logs page does nothing and throws: ``` TypeError: Cannot read properties of undefined (reading '_timestamp') at na (SearchResult.B_xPyti5.js:7:22212) at De (JsonPreview.CX_tRWWx.js:2:9172) ``` The same button in the row-detail drawer works, so the failure reads as intermittent. ## Root cause `JsonPreview` emitted `view-trace` with no payload, while the result grid's expansion slot bound the handler by reference: ```vue <template #expansion="{ row }"> <JsonPreview :value="row" … @view-trace="redirectToTraces" /> </template> ``` `redirectToTraces(log)` then read `log[store.state.zoConfig.timestamp_column]` off `undefined` — and `_timestamp` is that column's default value, which is exactly the reported error. The binding used to work because the table component in between re-emitted the event with the row attached (`emits("view-trace", row)`). That component was removed when the logs result grid migrated from `TenstackTable` to `OTable`, and the payload-less binding was carried over unchanged. The row-detail drawer resolves the hit in its own template, which is why only the inline expansion broke. The correlation drawer's log table (`CorrelatedLogsTable`) had the identical defect — not reported, same fix. ## Fix - `JsonPreview` emits the previewed log with the event, matching its sibling `show-correlation` emit, so every current and future listener receives the record. - Both expansion slots pass `row` explicitly, restoring the pre-migration semantics at the call site. - Both handlers no-op with a warning toast when the record is missing, so a visible, enabled button can never again be a silent dead end during triage. No change to when the button is shown, to the ±15 min window, or to the traces route contract. ## Tests Three regression specs, all failing before the fix with the exact `TypeError` above: - `JsonPreview.spec.ts` — the existing test only asserted *that* `view-trace` fired, never with what; it now asserts the payload, which is why the regression slipped through. - `SearchResult.viewTrace.spec.ts` (new) — expanded row emits a payload-less `view-trace`; asserts navigation to `traceDetails` with the right `trace_id`, `span_id`, and time window. - `CorrelatedLogsTable.viewTrace.spec.ts` (new) — same for the correlation drawer. Both new files are focused specs because neither existing suite installs a router or renders `OTable`'s expansion slot. ``` Test Files 8 passed (8) Tests 316 passed | 3 skipped (319) ``` `npm run type-check` → clean. `eslint` on the touched files → 0 errors. ## Verification Logs page with a trace-id-bearing stream and `service_streams_enabled` off: expand a row inline → **View Trace** lands on the trace with the right id and window. Same from the row-detail drawer and the correlation drawer's log table; console clean in all three. |
||
|
|
bf494ce1de |
fix(synthetics): move check variables into a shared resizable panel (#13699)
## What
Check variables lived inside the **Auth & Network** card on the
Configure page, so an author writing `{{token}}` references into journey
steps could not see them while editing. They now live in a
**`CheckVariablesPanel`** mounted as the right-hand pane on **both**
wizard pages (Journey + Configure), over one source of truth —
`check.variables`, **no schema change**.
## Panel behaviour
- **Cards** — name, masked value for secrets (raw value never rendered),
and a usage count computed from exact `{{name}}` matches across each
step's `value`, `selector`, and locator candidates. The "Used in N
steps" line only appears when the variable is actually referenced.
- **Add / edit** — inline, one form at a time, with the existing
validation rules (`^[A-Za-z_][A-Za-z0-9_]*$`, no duplicates). The add
form appends to the end of the list and scrolls into view; its button
stays pinned below the scroll region so it never scrolls away.
Re-pressing it while the form is open re-scrolls instead of discarding a
half-typed draft.
- **Delete** — confirmed via `ODialog`, and the dialog says what's at
stake: *"baseUrl will be removed from this check. It is referenced by 1
step, which will no longer resolve."* Still backed by the ~6s undo that
restores at the original index.
- **Empty state** — `OEmptyState` whose action is the sole Add
affordance while the list is empty.
- **Layout** — resizable via `OSplitter`; width is shared across both
pages through a new `useCheckWizardUi` composable. The Journey toolbar
gets a collapse toggle beside **Record**, following the field-list
pattern in `logs/SearchResult.vue`.
`CheckAuthNetwork` loses its Variables sub-section; its header summary
is now basic-auth only.
## Two OStepper library fixes this depended on
`OStep` is a multi-root component, so a `class` passed to it was
**silently dropped**. Every `h-full` below it then resolved against an
indefinite height — which left the splitter separator **0px tall and
impossible to grab**. (It predates this branch; step 1 already carried a
no-op `h-full!`.)
- `OStep` now forwards attrs explicitly — onto the **whole step** in
vertical mode, where they used to auto-inherit, and onto the content
panel in horizontal mode.
- `OStepper`'s horizontal pane gets `min-h-0` so a height-constrained
stepper clamps its content instead of growing with it (without it the
Configure separator measured 1818px and the panel was not truly pinned).
Verified the other horizontal-stepper consumers
(`CreateDestinationForm`, `OrgStorageEditor`) and the vertical
`BrowserJourneyStepEditor` are unaffected.
## Testing
**Automated** — 1147 tests pass across `components/synthetics`,
`views/synthetics`, `lib/navigation`, and `composables/synthetics`,
including a new 26-test `CheckVariablesPanel.spec.ts`
(add/edit/validation/masking/usage counts/scroll-to-form/confirm+undo
with fake timers). `type-check:app`, `lint:design:strict`, and ESLint
are clean — ESLint warning count is identical to baseline (0 errors).
**Manual, in the running app** — full flow driven end to end: gate →
journey → add with an invalid name rejected → valid add → splitter drag
(separator now 467px tall, drag moves it) → collapse/reopen preserving
the dragged width → step referencing `{{baseUrl}}` flipping the usage
badge to 1 → delete confirmation warning about the broken step → cancel
keeps it → confirm + undo restores it → Configure page showing the same
panel at the same width with the Auth & Network card carrying no
Variables section.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
a338597481 |
fix(lib): re-place toggle pill after an item reorder (#13721)
The sliding selection pill in OToggleGroup was re-measured only on a modelValue change or a track resize. A drag-to-reorder moves the active item but leaves the track exactly the same size, so neither trigger fired: the pill kept the previous item's position and width, landing under the wrong item and overlapping its neighbour, while the active text colour — being data-state driven — correctly followed the moved item. On the traces detail tab bar that read as the highlight and the colour coming apart. Observe the track's childList and snap-measure on a change, mirroring OTabs, which already did this for its underline. Attributes are deliberately not observed: data-state flips on a real selection change, which the modelValue watcher handles with a slide, and snapping there would cancel it. Also covers items being added or removed, e.g. the traces Thread tab appearing for a trace with LLM spans. |
||
|
|
087e1498ab |
feat(slos): add SLO burn-rate alerts (#13704)
design at /slo ## What Adds **SLO burn-rate alerts** — an alert family that fires off an SLO's error budget rather than a raw query — and finishes the SLO surface around it: navigation entry, detail-page fixes, and the flag removal now that SLOs have shipped. ## Changes **SLO burn-rate alerts (backend)** - New burn-rate evaluation (`config/src/meta/slo/burn.rs`, `condition.rs`) with multi-window burn conditions, and `core/src/slo/evaluate.rs` extended to evaluate them on the SLO job. - Alerts carry an `slo_id`; `infra/src/table/alerts` gains the column, filtering, and query support so an SLO's alerts can be listed and deleted with it. - Alert levels (`meta/alerts/level.rs`) and tags extended for the SLO family; scheduler handlers route SLO alerts through the same notification path. **SLO alert UI** - `SloAlertForm` + `AlertDetailSlo`: create, edit and view burn-rate alerts from the SLO detail page and the alert list. - Payload/routing helpers (`utils/alerts/sloAlertPayload.ts`, `sloAlertRouting.ts`, `alertPayload.ts`) with unit tests. - `AlertDestinationsField` extracted as the complete Destination form field — side label, tooltip, combined destinations/workflows picker, inline error, refresh, and create-in-new-tab. It replaces two verbatim copies in the generic alert form and is reused by the SLO form and synthetics `CheckAlerts` (which sets `supportsWorkflows: false`, since a check has no workflow routing). Fixes the SLO form's previously unwired refresh and Add Destination buttons. **SLO detail page fixes** - Burndown and burn-rate charts drew as bare axes when coverage was sparse: `showSymbol: false` means an isolated measured bucket between gaps has no segment and paints nothing. Symbols are now sized per point, and a sparse-coverage note states how many buckets actually recorded events. - Nothing scrolled below ~700px of viewport height — `OPageLayout`'s fixed flex body plus `OTabPanels` `scroll="none"` clipped the panels. Panels now grow and scroll; the stat strip and tab bar stay pinned. Verified at 1024x500, 1280x620, 1512x945. **Other** - `ZO_SLO_ENABLED` removed — SLOs have shipped, so the flag and its status/route gating are gone. - SLOs surfaced in the main nav. - Dashboard chart panel header gets its own tint via new `--color-panel-bar-bg` / `--color-panel-bar-border` tokens, and the bar is extracted into `PanelBar.vue` (it existed as five hand-copied class strings that had already drifted). - `tsconfig.vitest.tsbuildinfo` untracked; third-party vendor name removed from comments and test names. ## Testing - New Rust tests across `meta/slo/burn.rs`, `condition.rs`, `core/src/alerts/alert.rs` and `infra/src/table/alerts`. - New frontend suites: `sloAlertPayload.spec.ts`, `sloAlertRouting.spec.ts`, `alertPayload.spec.ts`, `AlertDetailSlo.spec.ts`, `SloDetailAlerts.spec.ts`, `SloListDelete.spec.ts`, `SloTimeSlicePreview.spec.ts`. - `cargo clippy --all-targets` and `web` `lint:ci` clean. |
||
|
|
d93d599918 |
chore: update translations from en-US.json (#13694)
Auto-generated translation updates from the English source file (`web/src/locales/languages/en-US.json`). 🤖 Generated with automated translation workflow --------- Co-authored-by: github-merge-queue[bot] <118344674+github-merge-queue[bot]@users.noreply.github.com> Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech> |
||
|
|
dd5ff2c131 |
feat(iam): serve the MCP Server setup page on the OSS build (#13720)
## Summary IAM > MCP Server was gated to enterprise/cloud, but the endpoint it documents is not. The backend registers api-endpoint outside the ent block and it initialises the MCP tools "for all editions" — only the OAuth *discovery* endpoints are enterprise-only. OSS users could reach the MCP server, just not the page that tells them how. Removes the build/runtime gates so the page ships everywhere, and hides the one part that genuinely is enterprise-only. ### Gates removed - `useEnterpriseRoutes.ts`: the `mcpServer` child was pushed inside the `isCloud || isEnterprise` branch, so on OSS the route did not exist at all. Moved into the base `children` array. - `IdentityAccessManagement.vue`: the tab's `visible` was `isEnt && zoConfig.ai_enabled`. Dropped. `ai_enabled` is `enterprise_value!(false, o2cfg.ai.enabled)`, i.e. always false on OSS, so that condition could never have let the tab through. - `useIngestionRoutes.ts` / `Recommended.vue`: the Ingestion > Recommended cross-link carried the same gate, explicitly to match where `mcpServer` existed. Un-gated to stay consistent. ### Card corrections - Drop the "MCP server is an Enterprise feature." note and delete the now-dead `ingestion.mcp.enterpriseNote` key from all 15 locale files. - OAuth *is* enterprise-only: `oauth_authorization_server_metadata` and `oauth_protected_resource_metadata` are `cfg(not(enterprise))` 404 stubs. The OAuth tab is now hidden on OSS and `authMode` defaults to `token`, rather than landing users on a flow that cannot complete. With one method left the tab picker is hidden too. The token path works unchanged on OSS: the snippets authenticate with `Basic base64(email:passcode)`. "Generate read-only credential" stays hidden there — `useMcpCredential` requires `rbac_enabled` to attach the read-only role — which degrades cleanly to the quick-start passcode and produces identical snippets. ### Tests Updated the OSS IAM child-count assertion (5 -> 6), the only test the change broke. Added OSS-presence coverage in `useEnterpriseRoutes.spec`, `useIngestionRoutes.spec`, `IdentityAccessManagement.spec` (with `ai_enabled` off) and `Recommended.spec`. Verified with `format:check`, `verify` (type-check:app + 5 lint gates, 0 errors) and 457 tests across the 7 affected specs. Refs #13717 |
||
|
|
e1aad76283 |
fix(dashboards): use bookmark-add for the panel annotation button (#13718)
The panel header's Add Annotation toggle rendered the same 'edit' glyph that the kebab menu uses for Edit Panel, so one shape stood for two unrelated actions on the same panel. Annotations are markLines pinned to a point on the x-axis, which bookmark-add states without borrowing the pencil. |
||
|
|
9cf4933f15 | fix: Browser sdk test 0.4.2 (#13584) |