MuroDocs
Tracking

Track custom events

Use window.muro.track() to record signups, purchases, CTA clicks, and any other moment that matters in your product.

Muro tracks pageviews and sessions on its own. For everything else, you fire a custom event from your code. There's exactly one API to learn.

The API

window.muro.track(eventName, props);
  • eventName: a short string that identifies the action, like 'signup' or 'purchase'.
  • props: an optional object with extra context, like the plan name or amount.

Two examples:

// A simple event with just a name.
window.muro.track('signup');

// An event with extra context.
window.muro.track('purchase', {
  plan: 'pro',
  amount: 19,
  annual: true,
});

That's the whole interface. No tracking plans, no event schemas to register, no SDK methods to chain.

Naming events

Stick to a few rules that pay off later when you're skimming a list of event names:

  • Use lowercase with underscores or hyphens. signup, cta_click, checkout-started. Pick one style and stick to it.
  • Use verbs or short noun phrases. signup, purchase, trial_started, invite_sent.
  • Stay consistent across your app. Don't fire signup in one place and Sign Up in another. They count as different events.
  • Keep names short. The maximum length is 64 characters, but most useful event names are under 25.

Event properties

The props object is for the context you'd want to filter by later. Keep it lean.

RuleLimit
Allowed value typesStrings, numbers, booleans only
Maximum keys per event10
Maximum serialized size2048 bytes

Nested objects, arrays, null, and undefined are not allowed and will be rejected. If you need a list, pick one representative value (for example, the first item) or flatten the keys.

// Good. Flat, primitive values.
window.muro.track('checkout_started', {
  plan: 'team',
  seats: 5,
  has_coupon: true,
});

// Won't work. Nested object is rejected.
window.muro.track('checkout_started', {
  user: { id: 'u_123', plan: 'team' },
});

Common recipes

These are the events most Muro users fire first.

Signup. Fire it once the account is actually created, not on form submit.

window.muro.track('signup', {
  plan: 'free',
  source: 'homepage',
});

Purchase or upgrade. Fire it from your success page or post-payment webhook callback in the browser.

window.muro.track('purchase', {
  plan: 'pro',
  amount: 19,
});

CTA click. For tracking which calls-to-action actually drive traffic.

document.querySelector('#hero-cta').addEventListener('click', () => {
  window.muro.track('cta_click', { location: 'hero' });
});

Trial started. If you have a separate trial flow.

window.muro.track('trial_started', { plan: 'pro' });

TypeScript

window.muro is added by the tracking script at runtime, so TypeScript doesn't know about it by default. Add a one-time declaration to a global types file (for example, src/types/global.d.ts):

declare global {
  interface Window {
    muro: {
      track: (
        eventName: string,
        props?: Record<string, string | number | boolean>,
      ) => void;
    };
  }
}

export {};

Now window.muro.track('signup') is fully typed across your project.

A few good-to-knows

  • Events fire even if the page unloads. Muro uses navigator.sendBeacon, so an event fired right before navigation or tab close still gets delivered.
  • Order matters less than you'd think. Custom events are batched and written within a few seconds of being fired. Don't worry about ordering across very-fast events.
  • You don't need to wait for muro to be defined. As long as the snippet is in your <head>, window.muro is available before any user-triggered code runs.

What's next

On this page