Skip to main content
Add one key to a node and that interaction is reported to the page’s analytics. You choose the moments, and nothing is measured unless you say so. Analytics here means what a person did in your experience: which items they opened, what they clicked, where they stopped. It is separate from platform monitoring, which measures whether the machinery is healthy.

The one key

analytics sits on a node, beside the action it accompanies.
Two fields. event is the name that arrives in the analytics tool, and params are the values sent with it. {{...}} reads the data already in scope at that node, which is the same binding an action’s values use. A node with no analytics block reports nothing. Silence is the default.

Moments a click does not cover

A card and its detail are one instance, and the detail opens several ways: the card is tapped, the workflow pushes it open, a suggestion lands on it. Reporting the tap alone misses the rest. So a view event is declared in the manifest, beside the lifecycle hooks, using their grammar. It fires when the state is entered, however it was entered:
manifest.yaml
layouts names the states that fire it, exactly as it does for a hook (Lifecycle hooks). The component already reports entering a state, so the declaration rides that moment and there is nothing to wire. A server action takes a manifest entry too, keyed by action instead of phase:
An action entry fires when your universe receives that action, rather than on the click, which an ad-blocker can silence, and rather than on the workflow finishing. The submission arrived, and that is the fact it reports. A button whose action stays on the page keeps its analytics key on the node. The platform already knows what opening something means: an app is done the moment it loads, and a component with a submit is started when it opens and finished when the submit arrives. You can name those moments yourself, or override them, with an entry keyed open:
An open entry fires when your universe opens the app for the person, before they have typed anything. done: true says the open is itself the finish. Without any entry the defaults apply: loaded for an app, started for a component with a submit, nothing for a component without one.

Naming events

Use the analytics tool’s own vocabulary wherever it has a word for what happened. GA4 defines view_item, generate_lead, select_item and others, and those names populate its built-in reports with no setup. Invent a name only when no standard one fits, and prefix it with unoverse_ so it cannot collide with an event the page already fires. One event per kind of thing. A course, a product and a service are the same moment: somebody looked at one thing in detail. Give them one event and separate them with a param.
Splitting them into view_course and view_product fragments the reporting. An analytics tool breaks one event down by param automatically, and it cannot recombine two event names into one funnel.

Choosing params

Send what answers a question somebody will ask, and leave out anything that only describes the machinery. Match the field names the customer’s own analytics already uses. Their reports and filters are built on those names, so a matching param joins work they have already done, while a generic name arrives as a column somebody has to map by hand.
A param that resolves to nothing is dropped, so an absent value is absent in the report rather than an empty column that reads as data. Three things never go in an event, and breaking any of them is a data protection problem rather than a bug:
  • No personal data. No name, email, phone or date of birth. Analytics tools prohibit it, and the property receiving it belongs to your customer.
  • No authored content. Report which item somebody opened, never what it said.
  • No typed text. A person’s question or form entry stays out, because both routinely contain personal detail.
An enquiry event carries an opaque reference to the record, never the record:

Connecting Google Analytics

You declare what happened, and the host page declares where it goes. That split is deliberate. A visitor’s analytics client id lives in a first-party cookie on the customer’s own domain, which code in the host page can read and a server call cannot. Sending from the server would file your events under a separate, unjoinable visitor: data that looks real and is unusable. Running in the page also inherits the consent state, regional configuration and retention terms the customer has already set, rather than reimplementing all of it per customer. Name the destination on window.unoverseConfig, beside the embed:
Which target to name. A bare GA4 snippet is gtag, because a dataLayer push only becomes a GA event under Tag Manager. A page running GTM is dataLayer. The target is never detected. A customer page routinely carries several analytics tools at once, and picking whichever global happens to exist would eventually push one customer’s behavioural data into a different vendor’s property. That is a data protection incident rather than a bug, so the destination is always named, and an absent one means silence, warned once.

Where events reach

Events fire on all three and only delivery differs, so with no listening host the post is a silent no-op rather than a broken call. Expect undercounting. Content blockers stop some of these calls, so never reconcile these numbers against server-side totals as though the gap were a bug.

Checking your events

Turn on debug for the channel you are testing, and each event prints as it fires.
Filter the browser console on unoverse:analytics to see only these.

When an event misbehaves

Next steps

Validate and ship

What the lint enforces, and what only you can judge.

analytics

The key, the four ready-made events, and turning it on.