Interaction Insights
- What Gets Captured
- Exposing the Endpoint
- Reading the Payload
- Sensitive Detail
- Replay Steps That Replay
- The Copilot Panel
- Fixing Insights with an AI Agent
- Configuration
- Insights, Metrics, and Traces
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, and for the errors browsers report — the one kind of failure the server never handled itself. 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. In development mode the same insights also appear in a Copilot panel, and their replay steps say what the user had filled in.
What Gets Captured
Three collectors run: one over user interactions, one over data provider queries, and one over the errors browsers report.
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).
Browser Errors
A script that fails in a tab reaches no server log at all.
The in-browser collector reports uncaught errors and unhandled rejections, and a third collector retains what identifies each one: the kind, the script it came from, and the first stack frame.
None of that is a number, and each would be one time series per distinct value, which is why it’s kept as an insight instead of as tags on the vaadin.client.errors counter.
This one additionally requires vaadin.observability.client (on by default), since the browser collector is what supplies the reports.
A payload carries at most 20 browser-error insights, the most-reported first.
Browser errors have a page of their own: see Client Error Insights for what a report carries, what reaches its script location, how a report that waited out an outage is reported, and why the payload is capped.
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, queries, and browser errors are retained separately, so with all three active the total is three times the capacity.
Keeping them apart means a burst of slow queries can’t evict the failed interactions, and neither can the flood of buffered reports that arrives when a network outage ends.
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
application.propertiesmanagement.endpoints.web.exposure.include=vaadinAdd 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 |
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(), ObservabilityKit.getRecentQueries(), and ObservabilityKit.getRecentClientErrors(), passing all three 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.
Browser errors group by route, error kind, script source, and stack frame.
The route is a template, so orders/17 and orders/18 group under one orders/:orderId insight instead of one per parameter value.
Five insight types can appear in the array:
type |
Meaning |
|---|---|
| A user interaction whose handler threw. |
| A user interaction that succeeded but ran over the UX budget. |
| A data provider count or fetch query that threw. |
| A data provider query that succeeded but ran over the UX budget. |
| An uncaught error or unhandled rejection a browser reported. The only insight type that can describe a failure the server never saw; see Client Error Insights. |
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.
The replay steps above are the production form, which names the component by its class.
In development mode they name it by its caption and list the state the view was in; see Replay Steps That Replay.
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 — and, for a browser error, the error message and the function name its stack frame named. 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
application.propertiesvaadin.observability.insights-details=trueThis 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.
The same property gates a browser error’s message and function name, but it behaves differently there: it governs collection rather than only retention, and it’s read by a page when it loads. See Sensitive Detail on the Client Error Insights page.
Replay Steps That Replay
In production, the replay steps of an interaction identify the interaction and no more, because that’s all a payload meant to be forwarded may say:
Source code
text
Open route '/returns'
Locate component Button
Trigger a 'click' event on it
Expect IllegalStateException: Inspection template 'defective' not foundWhich isn’t a reproduction. There may be four buttons on that view, and the failure may need a selection made before the click. In development mode the kit therefore reads two more things off the screen, and the steps become what a person would actually do:
Source code
text
Open route '/returns'
Set the 'Reason' Select to 'Defective'
Click the 'Process return' Button
Expect IllegalStateException: Inspection template 'defective' not foundThe two additions are:
- The caption of the interacted component
-
Its label,
aria-label, placeholder, own text,title, or — failing all of those — its id, so that a step names the one control the reader is looking for. It’s also inevidence.componentCaption, and the component class stays alongside it, because that’s what you grep for. An absentcomponentCaptionkey means the caption wasn’t collected, which is the case in production. - The state of the view
-
The values the view was holding, read at the moment the interaction was captured.
The View State Snapshot
The state is a snapshot, not a history.
Reading the values once, when the failure is captured, gives each field once, as it actually stood, in the order the user last changed them.
Accumulating what the user did as they did it isn’t equivalent: picking one item out of a Select arrives as three RPC invocations (opened-changed, value-changed, opened-changed), none of them an instruction anyone can carry out, and a user who changes their mind leaves the same field in the list twice with the stale value first.
What goes into the snapshot is every component in the view that holds a value, has a caption, and the user actually changed. The last of those is what keeps the list short: a replay starts from a freshly opened view, so a field nobody changed is already at the value the reader finds there, and telling them to set it is a line that says nothing. Only identity is remembered as the user works, never values; the values are read once, at capture.
"Changed" is decided by comparing the field’s value across the invocation, not by the event that carried it, so merely opening a dropdown doesn’t report it.
The exception is a synchronized property update: Flow applies the new value to the whole request’s state before it reports any invocation, so there’s nothing left to compare by then, and an mSync reaching something that holds a value is taken as a user edit.
The value is read from the component rather than from the property the client sent, so a Select says 'Defective' and not the item key '2'.
A field the user emptied becomes Leave the 'Order number' TextField empty, which is worth a line because a blank value is frequently the whole bug.
A value whose only text is a default toString() — com.example.Order@6f2b958e — is left out, since nobody can type that into a field.
At most ten values are reported per insight.
The scope is the view, not the page: the innermost route target holding the interacted component, so an application’s shell — its navigation, its app switcher — stays out. The shell is on screen throughout, has nothing to do with the failure, and would otherwise put the same lines in every insight the application produces. For a component the route target doesn’t hold, such as a dialog or overlay the UI owns directly, the scope is that component’s own top-level ancestor, which is the screen the user was actually looking at.
There are two things this doesn’t capture. A failure that needs a sequence rather than a state, such as clicking "Add line" twice: the state says what the view held, not how it got there. And a value the application set as a side effect of something the user did, such as picking a customer auto-filling their address, since what’s reported is what the user worked.
Grouping is unaffected. Occurrences still group by route, component, event, and exception, so the same failure hit with different values stays one insight, reporting the values of its most recent occurrence.
Why This Is Development Mode Only
Captions and values are withheld in production, where the steps fall back to the form at the top of this section, and no setting turns them on there. They’re application text that can be data-bound — a caption may read "Delete Jane Doe", and a field’s value is user input by definition — while the insights payload is built to be forwarded into issue trackers and AI agents. The reader who benefits from the detail is the developer with the application in front of them, so that’s the only mode that collects it.
Lengths are capped regardless: 60 characters for a caption, 40 for a value.
The Copilot Panel
In development mode the kit adds an Observability panel to Vaadin Copilot, reachable from the toolbar in edit, inspect, and test modes.
It opens on the findings rather than on the numbers: the same insights /actuator/vaadin/observability publishes — failed and over-budget interactions, failed and slow data queries, browser errors — ranked with errors first, then by how many users hit them, then by how recently.
Expanding one shows its evidence, its replay steps, and its suggestion, and Copy puts the whole finding on the clipboard as JSON, which is the shortest path from noticing a problem to handing it to an AI agent with access to the codebase.
The vaadin. meters sit below the findings, folded away while there’s something to look at, and grouped by the route they were recorded on: the route the browser is currently on first, then the rest alphabetically (the root view as *Root), the unresolved ones after them, and the application-wide meters that carry no route under General.
Route groups are matched against the browser’s location by route template, so orders/:orderId is the current group while you’re on /orders/17.
An application served under a context path matches nothing, and the groups stay alphabetical.
Findings You Aren’t Working On
"3 findings need attention" is only worth reading while all three are news, so two kinds fold away behind a collapsed line under the list — "2 hidden findings", "3 findings gone quiet", or "5 findings set aside (2 hidden, 3 gone quiet)":
- Hidden by hand
-
Every row has a Hide button, for the known slow query in the feature you aren’t touching today. It stays hidden even as the finding keeps recurring — a dismissal that undid itself on the next occurrence would be no dismissal at all — and is remembered in the browser’s
localStorage, so the reload that follows every code change doesn’t ask you to hide everything again. Unhide puts it back. - Gone quiet
-
A finding nothing has re-triggered for 30 minutes is history rather than attention. This one is automatic and reverses itself: the moment it recurs,
lastSeenmoves and it’s back in the count.
Nothing is discarded. The fold always shows how many findings are behind it and which of the two reasons put them there, and one click renders them, faded, with their detail and replay intact.
New Findings Announce Themselves
The panel keeps watching with its window closed, and a finding the payload didn’t have before is written to the Copilot log, deduplicated on the same grouping key the endpoint uses, so one problem notifies once however often it recurs. Announced are the findings the current page raised — anything first seen since it loaded, including during the load itself, so a slow query on the landing view is reported. The older records in the buffers, which outlive a reload, aren’t. A finding you hid isn’t announced either.
Findings need vaadin.observability.insights together with errors or requests, and client for browser errors — all on by default.
With any of them off, the panel says that insights aren’t being collected rather than showing an empty list.
Nothing here exists in production, where neither Copilot nor the dev-tools connection does.
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
applicationFramemay 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
suggestionorapplicationFrame, so tell the agent to find the data provider that the named component is given on the named route.
Configuration
| Property | Default | Description |
|---|---|---|
|
| Retain failed and over-budget interactions, data provider queries, and the errors browsers report.
Also requires |
|
| Allow retained interactions to carry the exception message, the top stack frames, and the raw session ID, and retained browser errors their message and function name. See Sensitive Detail. |
|
| Maximum number of retained records per buffer. Interactions, queries, and browser errors are retained separately, so with all three active the total is three times 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.
|
Traces | The full call tree of one occurrence, including nested navigation, data provider, and database spans.
The |
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