Muro
Get started

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. signup and Signup count as two different events. Pick one style and use it everywhere.
  • Use lowercase with _ or -. For example signup_completed, checkout_started, invite-sent. The Track an event dialog in the dashboard requires this style.
  • Name the outcome, not the button. signup_completed is clearer in six months than blue_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.

RuleLimit
Number of keysUp to 10
Value typesString, number, or boolean
Not allowednull, undefined, nested objects, arrays
Total sizeUp 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 to fetch with keepalive. 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

  1. Open your site, then open your browser's DevTools and go to the Network tab.
  2. Filter by collect.
  3. Click your button or trigger the action.
  4. You should see a POST to cdn.muroanalytics.com/collect with status 202. 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.

Next steps

On this page