Docs

Interaction Insights

How the insights endpoint turns a vague user report into a replicable interaction, with the route, component, event, and exception that caused it.

Metrics tell you that something is wrong; they don’t tell you what a user clicked. Interaction insights close that gap. Observability Kit retains the user interactions that went wrong — the ones that failed, and the ones that took too long — together with the route, the component, the event, and the exception behind them. It does the same for the data provider queries behind a slow lazy-loading component, which no interaction can account for. An endpoint then serves them grouped and ready to act on, so a report like "I clicked something on the orders page and got an error" becomes a concrete, replicable interaction.

The payload is a stable, machine-readable contract. Every insight carries a replay list a person can follow to reproduce the problem. An interaction insight also carries a suggestion and an applicationFrame that an AI agent with access to the codebase can open to verify the problem and propose a fix.

Insight collection is on by default and works in production mode.

What Gets Captured

Two collectors run, one over user interactions and one over data provider queries.

User Interactions

The interaction collector listens to server-side RPC invocations — the client-to-server calls behind a button click, a value change, an @ClientCallable method, or a return channel — and retains two kinds:

Failed interactions

An invocation whose handler threw. Requires vaadin.observability.errors (on by default), which supplies the failure path.

Slow interactions

An invocation that succeeded but took longer than the 1000 ms UX budget. Requires vaadin.observability.requests (on by default), which supplies the timing path. Beyond roughly a second a user stops feeling that they’re operating directly on the UI, so the budget is absolute rather than relative to a historical baseline. It’s also fixed: there is no property to change it.

Data Provider Queries

A slow data load never reaches the interaction collector. The invocation that triggers it only registers a flush, so a combo box that takes four seconds to fetch a page ends its invocation in microseconds and never qualifies as a slow interaction. A second collector therefore watches the queries themselves, retaining the ones that threw and the ones that ran over the same 1000 ms budget — a query is part of what the user waits for, so it earns attention at the same point.

This one additionally requires vaadin.observability.data (on by default).

What Happens to the Rest

Everything else is dropped. Retained records live in bounded in-memory ring buffers of vaadin.observability.insights-capacity entries each, 100 by default, and the oldest is evicted once a buffer is full. Interactions and queries are retained separately, so with both active the total is twice the capacity. Nothing is written to disk, and the buffers don’t survive a restart.

Collection is best-effort: if capturing a record fails, the error is swallowed rather than interfering with data loading or the framework’s own error handling.

Exposing the Endpoint

With the Spring Boot starter, the insights are served by an Actuator endpoint with the ID vaadin, at /actuator/vaadin/observability. Like every Actuator endpoint, it’s registered but not web-exposed until you say so:

Source code
application.properties
management.endpoints.web.exposure.include=vaadin

Add it to whatever else you already expose, for example prometheus,vaadin.

Exposure is all a standard setup needs, since Actuator endpoints allow access by default. If your application restricts endpoint access globally with management.endpoints.access.default, restore it for this endpoint with management.endpoint.vaadin.access=unrestricted.

Important

The payload describes what users did and what broke, so treat the endpoint as privileged. Secure it as you would any other Actuator endpoint — put it behind authentication, or bind the management port to an internal interface with management.server.port and management.server.address.

The endpoint is part of the Spring Boot starter and needs Actuator on the classpath. The payload is also available in-process: inject the VaadinObservabilityEndpoint bean and call section("observability") — for example to feed an admin view or an AI agent without going through HTTP.

In plain-Spring and standalone deployments the collectors still run, and you can read the buffers yourself through ObservabilityKit.getRecentInteractions() and ObservabilityKit.getRecentQueries(), passing both to an InsightsService to render the same payload.

Reading the Payload

A GET on the endpoint returns the current insights:

Source code
JSON
{
  "schemaVersion": 1,
  "generated": "2026-08-26T09:14:02.117Z",
  "instrumentation": "active",
  "insights": [ ... ]
}

instrumentation is active when at least one collector is bound, and inactive when the kit registered no instrumentation at all — for example when the license check failed or the feature is off. An empty insights array with instrumentation: active means the same thing it says: nothing went wrong. An empty array with instrumentation: inactive means nothing was watching.

Records are grouped, so ten users hitting the same problem produce one insight with ten occurrences. Interaction errors group by route, component, event, and exception type; slow interactions group by route, component, and event. Query errors group by route, component, query kind, and exception type; slow queries group by route, component, and query kind. The route is a template, so orders/17 and orders/18 group under one orders/:orderId insight instead of one per parameter value.

Four insight types can appear in the array:

type Meaning

user-interaction-error

A user interaction whose handler threw.

slow-user-interaction

A user interaction that succeeded but ran over the UX budget.

data-query-error

A data provider count or fetch query that threw.

slow-data-query

A data provider query that succeeded but ran over the UX budget.

A Failed Interaction

Source code
JSON
{
  "type": "user-interaction-error",
  "severity": "error",
  "category": "reliability",
  "summary": "User interaction 'click' on Button failed with NullPointerException (3 occurrences)",
  "evidence": {
    "route": "orders/:orderId",
    "component": "com.example.orders.OrderView$SaveButton",
    "event": "click",
    "rpcType": "event",
    "occurrences": 3,
    "firstSeen": "2026-08-26T08:51:44.002Z",
    "lastSeen": "2026-08-26T09:12:31.884Z",
    "exception": "java.lang.NullPointerException",
    "detail": "message and stack frames withheld; enable vaadin.observability.insights-details to collect them",
    "applicationFrame": "com.example.orders.OrderView.save(OrderView.java:88)"
  },
  "replay": [
    "Open route '/orders/17'",
    "Locate component SaveButton",
    "Trigger a 'click' event on it",
    "Expect NullPointerException"
  ],
  "suggestion": "Inspect com.example.orders.OrderView.save(OrderView.java:88); the 'click' handler in SaveButton throws NullPointerException. ...",
  "examples": [ ... ]
}

applicationFrame is the first stack frame that isn’t framework code — the JDK, Vaadin, Spring, Hibernate, the servlet container, and bytecode generators are all skipped — so it points at the application code most likely to hold the bug. The exception reported is the root cause, not the wrapper.

A Slow Interaction

Source code
JSON
{
  "type": "slow-user-interaction",
  "severity": "warning",
  "category": "performance",
  "summary": "Server handling of user interaction 'click' on Button took 2140 ms at the median (worst 3980 ms), over the 1000 ms UX budget (7 occurrences)",
  "evidence": {
    "route": "reports",
    "component": "com.example.reports.ReportView$ExportButton",
    "event": "click",
    "rpcType": "event",
    "occurrences": 7,
    "firstSeen": "2026-08-26T08:22:10.441Z",
    "lastSeen": "2026-08-26T09:13:57.203Z",
    "medianDurationMs": 2140,
    "maxDurationMs": 3980,
    "thresholdMs": 1000,
    "measures": "server-side RPC handling only; excludes session-lock wait, network transfer and client-side rendering"
  },
  "replay": [ ... ],
  "suggestion": "The 'click' handler in ExportButton occupies the request thread for about 2140 ms at the median ...",
  "examples": [ ... ]
}

The headline number is the median, not the worst case, so a single outlier doesn’t describe a group that’s only just over budget. Note what measures says: the duration is server-side invocation handling alone, so what the user actually felt is at least this much.

A Slow Data Query

A query insight describes a load rather than a user action, so its evidence carries a range and a row count where an interaction carries a DOM event and a stack frame:

Source code
JSON
{
  "type": "slow-data-query",
  "severity": "warning",
  "category": "performance",
  "summary": "The fetch query for OrderGrid takes 2310 ms (max 4120 ms), over the 1000 ms budget. The component cannot render until it returns, so this is time the user waits.",
  "evidence": {
    "route": "orders",
    "component": "com.vaadin.flow.component.grid.Grid",
    "queryKind": "fetch",
    "filtered": false,
    "requested": 200,
    "returned": 200,
    "occurrences": 4,
    "firstSeen": "2026-08-26T08:40:11.004Z",
    "lastSeen": "2026-08-26T09:11:52.617Z"
  },
  "replay": [
    "Open route 'orders'",
    "Load data into Grid",
    "Expect the fetch query to take around 2310 ms"
  ],
  "examples": [ ... ]
}

queryKind is fetch for a query loading one page of items, or count for one asking how many items a level holds. filtered separates a combo box loading matches for typed text from one loading the whole data set.

The remaining evidence depends on the kind. A fetch reports requested against returned, which is where over-fetching and short pages show up. A slow count reports counted instead — the total it arrived at — because "took four seconds" is far less actionable than "took four seconds counting 2,000,000 items".

A failed query is reported the same way as data-query-error, with an exception field holding the root cause and no duration figures. Query insights carry no suggestion or applicationFrame: the failing code is the data provider the component was given, which the kit can’t name from the query alone.

Examples

Each insight carries up to three of its most recent occurrences. For an interaction:

Source code
JSON
"examples": [
  {
    "timestamp": "2026-08-26T09:12:31.884Z",
    "location": "orders/17",
    "durationMs": 41,
    "sessionId": "5f2a91c40b7e",
    "uiId": 3
  }
]

location is the concrete path, as opposed to the template the insight groups on. sessionId is a short one-way hash by default: enough to tell whether three occurrences came from one user or three, without identifying the session. stackTop is present only when detail collection is enabled.

A query example is keyed differently — at rather than timestamp — and reports the range it asked for instead of a session:

Source code
JSON
"examples": [
  {
    "at": "2026-08-26T09:11:52.617Z",
    "durationMs": 4120,
    "offset": 0,
    "limit": 200,
    "rows": 200
  }
]

The offset, limit, and rows fields are present for a fetch and omitted for a count.

Sensitive Detail

The insights payload is meant to travel — into an issue tracker, an AI agent, a chat message — so anything that could carry personal or secret data is withheld unless you ask for it. By default an insight omits the exception message, the stack frames, and the raw session ID. What remains is still actionable: the route, the component, the event, the exception type, and the first application frame.

Turn the rest on when you need it:

Source code
application.properties
vaadin.observability.insights-details=true

This adds the exception message (truncated to 200 characters), the top five stack frames as stackTop, and the raw Vaadin session ID in place of the hash. An exception message is free-form text and can carry a whole payload, which is exactly why it’s opt-in.

Fixing Insights with an AI Agent

The payload is designed to be handed to a coding agent: every field an agent needs is machine-readable and versioned by schemaVersion. applicationFrame names the class, file, and line to open; replay lists the steps that reproduce the problem; and suggestion states a starting hypothesis grounded in the evidence.

Fetch the payload and hand it to an agent that has the codebase checked out:

Source code
terminal
curl -s http://localhost:8080/actuator/vaadin/observability | claude -p \
  "These are insights from the running application: user interactions \
that failed or blew the UX budget. For each insight, open the \
applicationFrame, verify the problem against the replay steps and the \
evidence, and propose a fix."

The example pipes the payload into the Claude Code CLI, but any agent that can read files and apply edits works the same way: paste the JSON into the conversation, or let the agent fetch the endpoint itself.

Three practicalities:

  • Point the agent at the same revision that produced the insights; otherwise the line number in applicationFrame may have drifted.

  • The default payload already carries what an agent needs — the route, component, event, exception type, and application frame — while withholding exception messages and session IDs, so it’s safe to forward to an external tool without enabling detail collection first.

  • A query insight carries no suggestion or applicationFrame, so tell the agent to find the data provider that the named component is given on the named route.

Configuration

Property Default Description

vaadin.observability.insights

true

Retain failed and over-budget interactions and data provider queries. Also requires errors for failures and requests for slow records; the query insights additionally require data.

vaadin.observability.insights-details

false

Allow retained interactions to carry the exception message, the top stack frames, and the raw session ID. See Sensitive Detail.

vaadin.observability.insights-capacity

100

Maximum number of retained records per buffer. Interactions and queries are retained separately, so with both active the total is twice this. The oldest is evicted once a buffer’s cap is reached.

In a standalone deployment, use the matching ObservabilitySettings builder methods — insights(), insightsDetails(), and insightsCapacity().

Insights, Metrics, and Traces

The three views answer different questions, and the same failing interaction shows up in all of them:

View What it tells you

Insights

Which interaction failed or was slow, on which route, in which component, and where in your code to look. Grouped, bounded, and current — not a time series.

Metrics

How often, and how the durations are distributed over time. vaadin.rpc.duration with outcome=error, the vaadin.errors counter, and the vaadin.data.* timers for query behavior.

Traces

The full call tree of one occurrence, including nested navigation, data provider, and database spans. The vaadin.rpc.<type> span carries the same component and event.

Insights need no backend and no dashboard, which is what makes them the fastest way from a user report to a line of code. For the meters and spans, see the Reference page.

7A1C4D82-6E35-4B90-8F2D-1B5E9C0A4736

Updated