From 0157be1d3a215ada1d438c8514503451b46b27df Mon Sep 17 00:00:00 2001 From: Mikael Grankvist Date: Mon, 21 Sep 2026 11:49:26 +0300 Subject: [PATCH] fix: update observability documentation (#5958) Update the documentation of observability-kit to match changes that made statements faulty. --- .../tools/observability/client-errors.adoc | 3 +- .../tools/observability/configuration.adoc | 15 +- .../tools/observability/getting-started.adoc | 6 +- articles/tools/observability/insights.adoc | 153 ++++++++++++++++-- .../tools/observability/migrating-to-v5.adoc | 2 +- articles/tools/observability/reference.adoc | 120 ++++++++++++-- 6 files changed, 266 insertions(+), 33 deletions(-) diff --git a/articles/tools/observability/client-errors.adoc b/articles/tools/observability/client-errors.adoc index e067cd264a..be9b92c58c 100644 --- a/articles/tools/observability/client-errors.adoc +++ b/articles/tools/observability/client-errors.adoc @@ -19,6 +19,7 @@ It's the one insight type that can describe a failure the server never handled - This page covers browser errors specifically. For failed and slow server-side interactions, the endpoint itself, and the payload envelope they share, see the <> page. +In development mode these insights also appear, alongside the server-side ones, in the <>. == Requirements @@ -242,7 +243,7 @@ A report that survives a reload keeps the wait it had accrued -- it's written to [NOTE] ==== The buffer holds at most 200 samples per tab, and both the buffer and the server's rate limit shed load by class rather than by age. -Browser errors rank second, behind the connection-state samples that explain an outage and ahead of bootstrap, navigation, and Web Vitals timing. +Browser errors rank second, behind the connection-state samples that explain an outage and ahead of every timing sample -- bootstrap, navigation, UIDL request and rendering, and Web Vitals. Two consequences: a full buffer drops timing samples before it drops the error behind a user's report, and a post-outage flush that exceeds [propertyname]`vaadin.observability.client-rate-per-session` loses timing samples rather than errors. A batch also stays in `sessionStorage` until the server has answered for it, and one nobody has answered for within 30 seconds is taken back and sent again, so a lost reply can't stall the collector for the life of the tab. diff --git a/articles/tools/observability/configuration.adoc b/articles/tools/observability/configuration.adoc index c3b5eab3bb..d19a4171a1 100644 --- a/articles/tools/observability/configuration.adoc +++ b/articles/tools/observability/configuration.adoc @@ -22,7 +22,7 @@ When you need to turn features off or tune them, how you configure the kit depen |`vaadin.observability.*` properties in [filename]`application.properties` (or any Spring property source). |Plain Spring -|Most of the same `vaadin.observability.*` keys, read with `@Value`. +|Most of the same `vaadin.observability.*` keys, read from the Spring [classname]`Environment`. See <<#plain-spring,Plain Spring>> for the ones it doesn't bind. |Standalone @@ -113,19 +113,20 @@ Off by default, since SQL is higher cardinality and can be sensitive. |`vaadin.observability.insights` |`true` -|Retain failed and over-budget user interactions for the insights endpoint. -Failures also need `errors`, and slow interactions also need `requests`. +|Retain failed and over-budget user interactions, and the errors browsers report, for the insights endpoint. +Failures also need `errors`, slow interactions also need `requests`, and browser errors also need `client`. See the <> page. |`vaadin.observability.insights-details` |`false` -|Allow retained interactions to carry the exception message, the top stack frames, and the raw session ID. +|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. Off by default, since the insights payload is meant to be forwarded. +For a browser error it governs collection rather than only retention, and is read by a page when it loads. |`vaadin.observability.insights-capacity` |`100` |Maximum number of retained records, applied to each buffer rather than shared. -Interactions and data provider queries are retained separately, so with both active the total is twice this. +Interactions, data provider 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. |`vaadin.observability.route-cardinality-limit` @@ -236,7 +237,7 @@ Also gates the data query insights. Also decorates the session error handler, which is what makes the failures Flow routes there countable and attributable to a component. |`client` -|Browser-observed signals -- bootstrap, navigation, and Web Vitals timing, client errors, and connection-state transitions with their downtime. +|Browser-observed signals -- bootstrap, navigation, UIDL request and rendering, and Web Vitals timing, client errors, and connection-state transitions with their downtime. |`resync` |A counter of UIDL message resends and client-requested resynchronizations, tagged by type. @@ -249,7 +250,7 @@ Spring Boot starter only, off by default. |Tracing spans for the request lifecycle, navigation, RPC, executor tasks, data provider queries, and -- with database monitoring on -- JDBC queries, emitted through the Observation API. |`insights` -|Retained failed and over-budget interactions and data provider queries, served by the insights endpoint. +|Retained failed and over-budget interactions, data provider queries, and browser errors, served by the insights endpoint. This one records no meters or spans; see the <> page. |=== diff --git a/articles/tools/observability/getting-started.adoc b/articles/tools/observability/getting-started.adoc index c89dca5f61..b3efbdc815 100644 --- a/articles/tools/observability/getting-started.adoc +++ b/articles/tools/observability/getting-started.adoc @@ -96,9 +96,11 @@ Then open `http://localhost:8080/actuator/prometheus` and look for the Vaadin me If you don't have an application yet, you can download one from https://start.vaadin.com[Vaadin Start], add the dependencies above, and run it. -During development you can also inspect the live meters without a backend. -When the application runs in development mode, the kit contributes a metrics panel to Vaadin Copilot that snapshots every `vaadin.*` meter -- counts, means, and current values -- straight from the running registry. +During development you can also inspect what the kit collected without a backend. +When the application runs in development mode, the kit contributes an *Observability* panel to Vaadin Copilot. +It leads with the findings -- the interactions that failed or ran over budget, the slow data queries, the errors browsers reported -- and keeps every `vaadin.*` meter below them, snapshotted straight from the running registry and grouped by the route it was recorded on. This panel is development-mode only and has no effect in production. +See <> for what it shows and how findings are ranked. For the full list of built-in meters, see the <> page. diff --git a/articles/tools/observability/insights.adoc b/articles/tools/observability/insights.adoc index 9b3c279e30..d98f0a8c0b 100644 --- a/articles/tools/observability/insights.adoc +++ b/articles/tools/observability/insights.adoc @@ -12,7 +12,7 @@ order: 35 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. +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. @@ -20,11 +20,12 @@ Every insight carries a [propertyname]`replay` list a person can follow to repro An interaction insight also carries a [propertyname]`suggestion` and an [propertyname]`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,Copilot panel>>, and their replay steps <<#dev-replay,say what the user had filled in>>. == What Gets Captured -Two collectors run, one over user interactions and one over data provider queries. +Three collectors run: one over user interactions, one over data provider queries, and one over the errors browsers report. === User Interactions @@ -46,11 +47,23 @@ A second collector therefore watches the queries themselves, retaining the ones 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 <> 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 [propertyname]`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. +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. @@ -81,7 +94,7 @@ Secure it as you would any other Actuator endpoint -- put it behind authenticati The endpoint is part of the Spring Boot starter and needs Actuator on the classpath. The payload is also available in-process: inject the [classname]`VaadinObservabilityEndpoint` bean and call [methodname]`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 [methodname]`ObservabilityKit.getRecentInteractions()` and [methodname]`ObservabilityKit.getRecentQueries()`, passing both to an [classname]`InsightsService` to render the same payload. +In plain-Spring and standalone deployments the collectors still run, and you can read the buffers yourself through [methodname]`ObservabilityKit.getRecentInteractions()`, [methodname]`ObservabilityKit.getRecentQueries()`, and [methodname]`ObservabilityKit.getRecentClientErrors()`, passing all three to an [classname]`InsightsService` to render the same payload. == Reading the Payload @@ -105,9 +118,10 @@ 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. -Four insight types can appear in the array: +Five insight types can appear in the array: [cols="1,3"] |=== @@ -124,6 +138,10 @@ Four insight types can appear in the array: |`slow-data-query` |A data provider query that succeeded but ran over the UX budget. + +|`client-error` +|An uncaught error or unhandled rejection a browser reported. +The only insight type that can describe a failure the server never saw; see <>. |=== @@ -162,6 +180,9 @@ Four insight types can appear in the array: [propertyname]`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 [propertyname]`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 <<#dev-replay,Replay Steps That Replay>>. + === A Slow Interaction @@ -281,7 +302,7 @@ The [propertyname]`offset`, [propertyname]`limit`, and [propertyname]`rows` fiel == 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. +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: @@ -295,6 +316,117 @@ vaadin.observability.insights-details=true This adds the exception message (truncated to 200 characters), the top five stack frames as [propertyname]`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 <> on the Client Error Insights page. + + +[[dev-replay]] +== Replay Steps That Replay + +In production, the [propertyname]`replay` steps of an interaction identify the interaction and no more, because that's all a payload meant to be forwarded may say: + +[source,text] +---- +Open route '/returns' +Locate component Button +Trigger a 'click' event on it +Expect IllegalStateException: Inspection template 'defective' not found +---- + +Which 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,text] +---- +Open route '/returns' +Set the 'Reason' Select to 'Defective' +Click the 'Process return' Button +Expect IllegalStateException: Inspection template 'defective' not found +---- + +The 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 in [propertyname]`evidence.componentCaption`, and the component class stays alongside it, because that's what you grep for. +An absent [propertyname]`componentCaption` key 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 [classname]`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 [classname]`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 [methodname]`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. + + +[[copilot]] +== 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, [propertyname]`lastSeen` moves 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. + [[ai-agents]] == Fixing Insights with an AI Agent @@ -330,18 +462,19 @@ Three practicalities: |`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`. +|Retain failed and over-budget interactions, data provider queries, and the errors browsers report. +Also requires `errors` for failures and `requests` for slow records; the query insights additionally require `data`, and the browser-error insights additionally require `client`. +In development mode a retained interaction also carries the caption of its component and the state its view was in; see <<#dev-replay,Replay Steps That Replay>>. |`vaadin.observability.insights-details` |`false` -|Allow retained interactions to carry the exception message, the top stack frames, and the raw session ID. +|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 <<#detail,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. +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. |=== diff --git a/articles/tools/observability/migrating-to-v5.adoc b/articles/tools/observability/migrating-to-v5.adoc index 6aaf7b2ed5..1f6bc4f83e 100644 --- a/articles/tools/observability/migrating-to-v5.adoc +++ b/articles/tools/observability/migrating-to-v5.adoc @@ -300,7 +300,7 @@ The most common renames: |(none) |`vaadin.client.*` -|Browser-observed timing (bootstrap, navigation, Web Vitals, errors). +|Browser-observed timing (bootstrap, navigation, UIDL requests and rendering, Web Vitals), plus browser errors and connection-state transitions. |(none) |`vaadin.resync` diff --git a/articles/tools/observability/reference.adoc b/articles/tools/observability/reference.adoc index bcbda6c23c..d2c09e0b43 100644 --- a/articles/tools/observability/reference.adoc +++ b/articles/tools/observability/reference.adoc @@ -211,6 +211,7 @@ Controlled by `vaadin.observability.requests`. |Timer |Server-side request handling time. Tagged by `vaadin.request.type`, `vaadin.interaction`, `http.method`, `outcome`, and `error`. +See <<#request-types,Request Types>> for what the type tag classifies. |`vaadin.rpc.duration` |Timer @@ -230,6 +231,50 @@ The span's `ui.id` and `vaadin.client.location` attributes are unbounded and are ==== +[[request-types]] +=== Request Types + +Every request Vaadin handles is classified before it's timed, and the class becomes the `vaadin.request.type` tag on `vaadin.request.duration` -- and, with tracing on, the `vaadin.request.` span name. +The types differ so much in what they do that a single average across all of them means nothing. + +[cols="1,3"] +|=== +|`vaadin.request.type` |What It Covers + +|`uidl` +|A UI interaction: the request the client sends for a click, a poll, or a navigation. +The one type broken down further, by the `vaadin.interaction` tag. + +|`bootstrap` +|A page load: the HTML document request, and the `init` request the client engine follows it with to have the UI created. +The server's side of `vaadin.client.bootstrap.duration`. + +|`stream` +|A download or an upload, served by Flow's stream request handler. +A transfer is expected to be long-running, which is why it's kept out of the other buckets: averaged in with page loads, it both hides its own outliers and inflates theirs. + +|`push` +|A push channel request, on any transport. + +|`heartbeat` +|The keep-alive the browser sends for an open UI. + +|`static` +|A static resource: `/VAADIN/`, `/static/`, `/themes/`, or `/sw.js`. + +|`other` +|Everything left, an application's own endpoints under the Vaadin servlet among them. +|=== + +A page load is recognized from the browser's `Sec-Fetch-Dest` header, falling back to an `Accept` header that asks for `text/html` first for browsers old enough not to send it. +A `fetch()` to an application endpoint that happens to sit under the Vaadin servlet is therefore not counted as one. +The classification is deliberately conservative in that direction: a page load that can't be told apart from application traffic stays `other` rather than diluting `bootstrap`. + +An embedded route counts as a page load too. +A request whose destination is an `iframe`, `frame`, `embed`, or `object` is served the same [filename]`index.html` and builds a UI of its own, and `vaadin.client.bootstrap.duration` records it from the browser's end as well. +A view that embeds another of its own routes therefore reports a second `bootstrap`, which is the second UI it really does build. + + === Resync Metrics Controlled by `vaadin.observability.resync`. @@ -303,6 +348,16 @@ These are observed in the browser and reported back to the server, subject to a |Timer |Browser-observed navigation time. +|`vaadin.client.request.duration` +|Timer +|One UIDL request as the browser saw it, from the moment the request was queued to the last byte of the response. +The browser's side of `vaadin.request.duration`; see <<#client-interaction-timing,Interaction Timing From the Browser>>. + +|`vaadin.client.render.duration` +|Timer +|How long Flow's client spent applying one UIDL response to the page. +Recorded only when Flow's `requestTiming` deployment setting is on, which is the default outside production mode. + |`vaadin.client.web_vitals.lcp` |Timer |Largest Contentful Paint. @@ -315,6 +370,7 @@ These are observed in the browser and reported back to the server, subject to a |Counter |Errors reported by the browser. Tagged by `kind`. +What identifies one -- the message, the script it came from, and the first stack frame -- is kept as an insight rather than as tags; see <>. |`vaadin.client.connection` |Counter @@ -334,18 +390,54 @@ Untagged. |`vaadin.client.dropped` |Counter -|Client samples dropped before recording, for example a sample submitted under a name that isn't on the ingest allowlist. +|Client samples that reached ingest but never made it into a meter: one the registry refused, and one whose reported duration isn't a measurement at all -- negative, not finite, or over an hour. +A timer's sum only ever grows, so one saturating value would skew its average for the life of the process. Untagged. |=== The client timers -- except `vaadin.client.connection.downtime`, which carries `state` instead -- are tagged by `route`, resolved from the browser location to a route template on the server and capped by the same cardinality limit as the server-side meters. `vaadin.client.navigation.duration` additionally carries `trigger`. -Server round-trip timing isn't collected in the browser. -Use the server-side `vaadin.request.duration` and `vaadin.rpc.duration` timers for that. - The browser buffers samples and flushes them every five seconds, and when the page is hidden. -Only the meters in the table above are accepted; a sample under any other name is dropped at ingest, which caps the cardinality a buggy or malicious client can create. +Only the meters in the table above are accepted; a sample under any other name is discarded at ingest without being recorded or counted, which caps the cardinality a buggy or malicious client can create. + + +[[client-interaction-timing]] +=== Interaction Timing From the Browser + +When a user says a click took a second and `vaadin.request.duration` says the server took forty milliseconds, the other nine hundred and sixty are somewhere the server can't see: on the wire, in the browser's request queue, or in the browser applying the response. +Two of the client meters make that remainder readable, with no configuration beyond `vaadin.observability.client`. + +`vaadin.client.request.duration` is the same round trip as `vaadin.request.duration`, measured at the browser's end. +It's read off the Resource Timing entry every UIDL `POST` leaves behind, so it needs nothing from Flow and works in production. +Subtract the server's figure for the same route, and what's left is the network: + +[source,text] +---- + rate(vaadin_client_request_duration_seconds_sum[5m]) +/ rate(vaadin_client_request_duration_seconds_count[5m]) +- + rate(vaadin_request_duration_seconds_sum{vaadin_request_type="uidl"}[5m]) +/ rate(vaadin_request_duration_seconds_count{vaadin_request_type="uidl"}[5m]) +---- + +`vaadin.client.render.duration` is the third segment, after the network and the server: how long Flow's client spent applying the response to the page. +A response that arrives in fifty milliseconds and takes four hundred to render is a browser problem -- typically a heavy component tree or an expensive renderer -- and neither of the other two timers can show it. +The figure is Flow's own, published through [methodname]`getProfilingData()` on each client only when Flow's `requestTiming` deployment setting is on. +That's the default outside production mode; in production, set `vaadin.requestTiming=true` to record this meter. +Without it the meter is absent rather than zero. + +Four things are worth knowing about both: + +Only UIDL requests are timed:: Heartbeats, push, and static resources aren't interactions and are left out. +The collector's own request that carries the samples to the server is left out too, so the kit doesn't report itself. + +The request meter needs the default transport:: It's read from Resource Timing, which sees HTTP requests. +With [annotationname]`@Push(transport = WEBSOCKET)` the UIDL rides the websocket and leaves no entry, so only the render meter is recorded. + +Bootstrap isn't an interaction:: The first UIDL response, which builds the page, is covered by `vaadin.client.bootstrap.duration` and excluded here. + +The route is the browser's:: Both meters carry the route the browser was on when the response arrived, which for a navigation request is the view navigated to, as on the server's request span. [[data-metrics]] @@ -484,7 +576,8 @@ The kit produces the following spans: |`vaadin.request.` |The root span for each Vaadin request. -A UIDL request is named by its interaction -- `vaadin.request.rpc`, `vaadin.request.poll`, or `vaadin.request.navigation` -- and other requests by their type: `vaadin.request.heartbeat`, `vaadin.request.push`, `vaadin.request.static`, or `vaadin.request.other`. +A UIDL request is named by its interaction -- `vaadin.request.rpc`, `vaadin.request.poll`, or `vaadin.request.navigation` -- and other requests by their type: `vaadin.request.bootstrap`, `vaadin.request.stream`, `vaadin.request.heartbeat`, `vaadin.request.push`, `vaadin.request.static`, or `vaadin.request.other`. +See <<#request-types,Request Types>>. Carries the request-level attributes below. |`vaadin.navigation ` @@ -493,7 +586,7 @@ Carries the request-level attributes below. |`vaadin.rpc.` |A server-side RPC invocation (DOM event, [annotationname]`@ClientCallable`, property sync, or return channel), nested under the request. -|`vaadin.ui.access` +|`vaadin.executor.task` |One task run on the Vaadin service executor, nested under whatever trace was active when the task was submitted. See <<#background-work,Background Work>>. @@ -517,10 +610,12 @@ With both data provider and database monitoring on, a slow interaction opens up === Background Work With tracing enabled, the kit wraps the Vaadin service [classname]`Executor` so that the trace context active when a task is *submitted* is restored when the task *runs*. -A background task started from a request thread therefore stays in the same trace across the thread hop, under its own `vaadin.ui.access` span, instead of appearing as an unrelated root span. +A background task started from a request thread therefore stays in the same trace across the thread hop, under its own `vaadin.executor.task` span, instead of appearing as an unrelated root span. -This covers the executor Vaadin dispatches signal effects and result notifications on, and that applications are expected to use for their own background work -- typically a task that ends by pushing its result through [methodname]`UI.access()`. -It isn't [methodname]`UI.access()` itself: a command queued with [methodname]`UI.access()` runs on whichever thread unlocks the session, and is recorded there. +This covers the tasks submitted to that executor: the signal effects Vaadin re-evaluates on it, the signal result notifications it dispatches through it, and the background work applications are expected to hand to it -- typically a task that ends by pushing its result through [methodname]`UI.access()`. +It isn't every [methodname]`UI.access()` call: a plain call from a background thread queues a command that whichever thread unlocks the session drains, so it never passes through the executor and gets no span of its own. +A notification task is itself a [methodname]`UI.access()` call, so its span always covers the dispatch, but covers the notification body only when the session lock happens to be free. +Otherwise the task returns as soon as the command is enqueued, and the body runs later on the unlocking thread, outside the span. Work you hand to an executor of your own isn't wrapped. To keep such work in the trace, submit it through the Vaadin service executor, or propagate the context yourself with Micrometer's [classname]`ContextSnapshot`. @@ -535,7 +630,8 @@ The root `vaadin.request` span carries these attributes: |Attribute |Description |`vaadin.request.type` -|The protocol-level request type: `uidl`, `heartbeat`, `push`, `static`, or `other`. +|The request type: `uidl`, `bootstrap`, `stream`, `push`, `heartbeat`, `static`, or `other`. +See <<#request-types,Request Types>>. |`vaadin.interaction` |What the request actually did: `poll`, `navigation`, or `rpc`, and `none` for requests where no interaction applies, such as heartbeats and static resources. @@ -567,7 +663,7 @@ Those spans carry `type` and `vaadin.rpc.component`, but no `vaadin.rpc.event`. The `vaadin.db.query` span carries `route`, a `db.rows` attribute with the number of rows read, and -- when [propertyname]`vaadin.observability.database-statement` is enabled -- the parameterized SQL as `db.statement`. -The `vaadin.ui.access` span carries no attributes of its own. +The `vaadin.executor.task` span carries no attributes of its own. Its value is structural: it shows where a submitted task ran, under the trace it was submitted from.