Send analytics events
One callback, five events, no provider — wire duxt's reader interactions into the analytics you already run.
duxt reports what readers do with a documentation site — searching, copying a command, trying an endpoint, answering "Was this page helpful?" — to one function you write. It ships no provider, no SDK and no script, and a site that has not written that function reports nothing and downloads nothing extra.
There is no default destination, because a documentation layer cannot pick one
for somebody else's readers. Without duxt.analytics.track, every call site
below reaches a missing function and stops.
Steps
- Write the callback. It lives in
app.config.ts, beside the rest of your duxt config, and it is handed one event per interaction:export default defineAppConfig({ duxt: { analytics: { track(event) { // `event.name` is the discriminant; `event.context` says which // page, locale, source and version it happened on. console.log(event.name, event.context.path); } } } });
The callback runs in the browser only. A build and a prerender call it never, so a static render does not fill your analytics with pages a crawler opened. - Send it where your analytics already live. Whatever your provider
exposes — an SDK already on the page, or a route of your own so the reader's
browser never talks to a third party you did not choose:
export default defineAppConfig({ duxt: { analytics: { track(event) { const { name, context, ...properties } = event; // Your provider, your property names, your consent gate. window.plausible?.(name, { props: { ...context, ...properties } }); } } } });
duxt sets no cookie, stores nothing, buffers nothing and shows no consent
banner — it has nothing to ask about, because nothing leaves the page except
through your function. That also means the gate is yours: if your privacy
policy requires consent before this reaches a provider, check it inside
track and return early.
- Pick the events you want, and drop the rest. The union is closed, so a
switchcovers it exhaustively and anything you do not name is simply not sent:
track(event) {
if (event.name === 'search' && event.results === 0) {
// The one report a documentation site nearly always wants: a term
// somebody looked for and the site could not answer.
report('docs-search-miss', { query: event.query, ...event.context });
}
}
- Check one event arrives, with the context you expected — under a version and locale prefix, not just at the bare URL.
The events
Every event has a name, its own fields, and a context.
name | Emitted when | Carries |
|---|---|---|
search | A debounced, non-empty query settles — including one that found nothing | query, results, approximate |
search-result | A reader opens a hit | query, rank, to, collection |
copy | A layer-owned code example is copied | kind, language, manager, sample |
api-request | A try-it request settles | operation, method, path, outcome, statusClass, duration |
feedback | A reader answers "Was this page helpful?" | helpful |
search fires once per settled query, not per keystroke: the debounce drops
what nobody finished typing, and a result superseded by a newer one is never
reported, because the reader never saw it.
copy covers the layer's own code examples — a fence, a ::code-group, a
::package-managers block, a request sample in the try-it client — with kind
saying which. "Copy page as Markdown" is not in this family and emits
nothing: it is the page, not an example in it.
api-request describes the operation as the OpenAPI document declares it.
path is the template (/pets/{id}), never the URL that was called.
The context
| Field | Notes |
|---|---|
path | The documentation path, without the locale prefix, query string or fragment |
locale | The locale the page was read in |
source | Which source the page came out of — absent on a single-source site |
version | Which version — absent where the source has only one |
What is never sent
This list is the point of the feature, so it is worth stating plainly. No event carries:
- an API credential, header or token;
- a request or response body, or a concrete request URL;
- the text that was copied;
- a search result's rendered content;
- a browser, session or reader identifier of any kind;
- the query string or fragment off the page's own path.
duxt also never transmits, buffers or persists anything itself. There is no queue, no retry, no storage and no network call in the layer: your function is the only thing that ever leaves the page.
Your callback is never awaited, and whatever it throws or rejects with is swallowed — a provider that is down must not take the search, the copy, the feedback or the request down with it.
Page feedback, either way
feedback is the same answer DuxtPageFeedback has always emitted. The
component's @feedback event and its default slot are unchanged, so
wrapping it still works and neither route needs
the other — use the analytics callback if you want one destination for
everything, and the wrapper if feedback alone goes somewhere of its own.
Checklist
-
duxt.analytics.trackis set, and nothing else was needed to turn it on - The destination is yours — a provider you chose, or a route of your own
- Consent is checked inside
track, if your policy asks for it - An event arrived with the path, locale, source and version you expected
- Nothing in what you forward on identifies the reader