Track custom events
Record signups, purchases, button clicks, and any other action that isn't a pageview.
Muro tracks pageviews on its own. For anything else, like a signup, a payment, or a click on a button, you send a custom event. There are two ways: an HTML attribute for clicks, and one JavaScript call for everything else.
Custom events appear in the Custom Events panel on your dashboard and in session journeys. Make one a goal to count it as a conversion.
Track a click with an attribute
Add data-muro-event to any button, link, or element:
<button data-muro-event="start_trial_click">Start free trial</button>Every click on the element, or on anything inside it, sends a custom event with that name. No JavaScript needed.
- It works for elements added to the page later, and across page changes in single-page apps.
- It still fires if your own click handler stops the event from bubbling.
- If marked elements are nested, the closest one to the click wins, so one click sends one event.
- It only reads the attribute. It never changes your page or the element's behaviour.
- It can't send properties. If you need them, use
track()instead.
In JSX and Vue templates you write the attribute the same way. If the clickable thing is a component like <Button>, make sure the attribute reaches the real HTML element. Most UI libraries pass data-* attributes through. If yours doesn't, put the attribute on a wrapping element.
A click is not the same as a completed action. Clicks on Sign up include people who then abandon the form. To count completed signups, call track() once the signup succeeds.
Track anything with track()
Call window.muro.track() from your code at the moment the action succeeds:
window.muro?.track('signup_completed');You can add properties as a second argument:
window.muro?.track('signup_completed', {
plan: 'pro',
billing_cycle: 'annual',
});Always write window.muro?.track(…) with the ?.. The tracking script loads asynchronously, so window.muro only exists once the script has run. It also won't exist if an ad blocker stopped the script. With ?., your code never throws in either case. Muro doesn't queue calls made before the script loads, so those events are lost. In practice this only affects events fired in the first moments of a page load.
Name your events
- The name is required and can be up to 64 characters.
- Names are case-sensitive.
signupandSignupcount as two different events. Pick one style and use it everywhere. - Use lowercase with
_or-. For examplesignup_completed,checkout_started,invite-sent. The Track an event dialog in the dashboard requires this style. - Name the outcome, not the button.
signup_completedis clearer in six months thanblue_button. - Reuse a name for the same action. If the same button appears in the header and on the pricing page and both should count together, give them the same name.
Properties
Properties add context to an event. They're optional.
| Rule | Limit |
|---|---|
| Number of keys | Up to 10 |
| Value types | String, number, or boolean |
| Not allowed | null, undefined, nested objects, arrays |
| Total size | Up to 2,048 characters as JSON |
If an event breaks any of these rules, Muro drops the whole event, not just the bad property. Nothing errors on your page, so test new events once with the steps in Verify it works.
// Works: flat, simple values
window.muro?.track('checkout_started', {
plan: 'team',
seats: 5,
has_coupon: true,
});
// Dropped: nested object
window.muro?.track('checkout_started', {
user: { id: 'u_123', plan: 'team' },
});Goals and the Custom Events panel count events by name. Properties are stored with each event, but they don't split the counts on the dashboard.
Never put personal data in event names or properties: no email addresses, names, phone numbers, or user IDs that identify a person.
Recipes
Completed signup. Fire it after your server confirms the account was created, not when the form is submitted.
async function handleSignup(form) {
const res = await fetch('/api/signup', { method: 'POST', body: new FormData(form) });
if (res.ok) {
window.muro?.track('signup_completed', { plan: 'free' });
}
}Purchase. Fire it on your order confirmation page, or after the payment call succeeds.
window.muro?.track('purchase', { plan: 'pro', amount: 19 });Call-to-action click. No JavaScript needed.
<a href="/signup" data-muro-event="hero_cta_click">Get started</a>File download.
<a href="/guide.pdf" download data-muro-event="guide_download">Download the guide</a>Newsletter signup in React. Fire it once the request succeeds.
async function onSubmit(email: string) {
const res = await fetch('/api/newsletter', {
method: 'POST',
body: JSON.stringify({ email }),
});
if (res.ok) window.muro?.track('newsletter_subscribed');
}Note that the email goes to your own API, not to Muro. The event only carries its name.
TypeScript
The tracking script adds window.muro at runtime, so TypeScript doesn't know about it. Add this declaration once, for example in src/types/muro.d.ts:
declare global {
interface Window {
muro?: {
track: (
eventName: string,
props?: Record<string, string | number | boolean>,
) => void;
};
}
}
export {};Declaring muro as optional means TypeScript reminds you to write window.muro?.track(…), which is the safe form.
Turn an event into a goal
Once an event shows up in the Custom Events panel, hover it and click Mark as goal. From then on it counts toward Conversions and Conv. Rate, including past events.
If you haven't added the code yet, click Track an event in the Custom Events panel instead. It writes the code or a prompt for your AI coding assistant, and lets you save the goal before the first event arrives. See Set up goals.
The Custom Events panel
The panel lists every custom event name your site sent in the selected period, with the columns Event, Trend, Count, Visitors, and Last seen. View all shows up to 200 names and adds First seen.
The panel follows your dashboard filters. To filter the whole dashboard by an event, make it a goal and click the goal in the Goals panel.
If you use the Shopify pixel, its add_to_cart, checkout_started, and purchase events appear here too.
Good to know
- Events survive page unloads. Muro sends events with
navigator.sendBeacon, falling back tofetchwithkeepalive. An event fired right before a link navigates away, or before the tab closes, still arrives. - "muro is not defined". You called
window.muro.track()without?.before the script loaded. Add the?.. See muro is not defined. - Device clocks matter. Events are timestamped by the visitor's device. Events from a device whose clock is more than 10 minutes ahead are rejected.
- No cookies. Custom events use the same daily-rotating visitor ID as pageviews. See Privacy.
Verify it works
- Open your site, then open your browser's DevTools and go to the Network tab.
- Filter by
collect. - Click your button or trigger the action.
- You should see a
POSTtocdn.muroanalytics.com/collectwith status202. Its payload has"event_type":"custom"and your event name.
Then set your dashboard to Live. The event appears in the Custom Events panel within about 30 seconds.