Test
Goals
Count clicks, forms, and thank-you pages, send revenue, and guard what matters.
A goal is a named action you want visitors to take: clicking a button, sending a form, or reaching a thank-you page. You record goals in your pages, and each experiment is judged on one of them. An experiment can also watch up to five secondary goals, to catch a change that helps one number and hurts another.
There are four ways to record a goal. Clicks and handlers run in the visitor's browser. Forms and thank-you pages are recorded by the server, so they work without JavaScript.
Clicks#
track="…" on any element records the goal when the element is clicked. A click on anything inside it counts too.
import { Page } from "mockly"; export default function Pricing() { return ( <Page title="Pricing · Acme"> <h1>Simple pricing</h1> <a href="/signup" track="signup"> Start free </a> <a href="https://cal.example.com/acme/demo" track="demo-request"> Book a demo </a> </Page> );}It works on links that leave the page, including links to other sites, because the browser sends the goal in the background as the page unloads. The value must be a string literal (MK4012).
From an event handler#
track("…") records a goal from an event handler, for actions that aren't a plain click on one element: a choice in an interactive part, a toggle, a key press.
import { state, track } from "mockly"; export function BillingToggle() { const yearly = state(false); return ( <button type="button" onClick={() => { yearly.value = !yearly.value; if (yearly.value) { track("chose-yearly"); } }} > {yearly.value ? "Billed yearly" : "Billed monthly"} </button> );}track() works only inside event handlers (MK3011), and takes the goal name as a string literal (MK4012). See Interactive parts for handlers and state.
Sending a value#
A second argument sends an amount with the goal, such as an order's total. An experiment with metric="revenue" compares these amounts (see Revenue):
import { state, track } from "mockly"; export function CheckoutButton() { const total = state(49.5); return ( <button type="button" onClick={() => { track("purchase", total.value); }} > Pay {total.value} </button> );}The value is any number expression, and the types check it: a string is TS2345. It's sent when it's a finite number of 0 or more, and kept up to 1,000,000,000. Anything else sends the goal without a value, so a bad amount never loses a conversion.
Thank-you pages#
goal on <Page> records the goal each time a visitor is served the page:
import { Page } from "mockly"; export default function Thanks() { return ( <Page title="Thanks · Acme" goal="signup" noindex> <h1>Thanks for signing up</h1> <p>Check your inbox for a link to get started.</p> </Page> );}The server records it as it sends the page. So it works without JavaScript, no ad blocker or closed tab can lose it, and it counts when another site sends visitors to the page, such as a scheduler, a checkout, or a form on your CRM that redirects when it's done. The value must be a string literal (MK4016).
To send an order's total with it, have your checkout redirect to the page with mk_value in the address, such as /thanks?mk_value=49.99. It's read only for the goal: digits, with up to six decimals, at most 1,000,000,000. The page itself never sees it.
Forms#
A <Form> records its goal when it's submitted, also on the server, with or without JavaScript. The goal is the form's name unless you set goal.
import { Form } from "mockly"; export function SignupForm() { return ( <Form name="signup" redirect="/thanks" class="space-y-4"> <input type="email" name="email" aria-label="Email" required /> <button type="submit">Join the waitlist</button> </Form> );}This form records signup. See Forms for redirects, messages, and webhooks.
Goal names#
Goal names use lowercase letters, digits, and dashes, start with a letter or a digit, and are at most 63 characters long (MK4002).
The same name is the same goal wherever it's recorded. A track="signup" link on the home page and a <Form name="signup"> on the sign-up page both record signup.
A goal doesn't have to be on the page with the experiment. A visitor who sees an experiment on the home page and signs up on another page counts as a conversion for that experiment.
How conversions are counted#
For each experiment, results count:
- Each visitor once, in the variant they were first assigned.
- A conversion when that visitor reaches the goal at or after the first time they were served the experiment, on any page. Reaching it again doesn't count again.
- Nothing from before: reaching the goal before seeing the experiment doesn't count.
- Only visitors since the last restart: when an experiment's results restart, visitors who saw it before stop counting. See Reading results.
Previews in the studio and visits from bots aren't counted.
The Goals table on the Analytics page counts differently. It shows the visitors who reached each goal and every completion, so a visitor who clicks twice adds two completions there.
Secondary goals#
A headline can win clicks and still lose sign-ups. To watch for that, list secondary goals next to the main one:
import { Experiment, Page, Variant } from "mockly"; export default function Home() { return ( <Page title="Acme"> <Experiment name="hero" goal="cta-click" secondary={["signup", "demo-request"]}> <Variant name="control"> <h1>Ship faster</h1> </Variant> <Variant name="bold"> <h1>Ship 10x faster</h1> </Variant> </Experiment> <a href="/signup" track="cta-click"> Start free </a> </Page> );}- List up to five goals, each once, without the experiment's own goal (MK4019).
- Each should be recorded somewhere in the site (MK4010).
- Experiments that share a name list the same secondary goals, in any order (MK4017).
The results show each secondary goal the way they show the main one: each variant's conversion rate, and its lift and difference against control, with intervals that stay valid however often you check. Those intervals are adjusted together, across every variant and every secondary goal, so watching more goals doesn't raise the chance of a false alarm. Each goal you add makes every secondary goal's intervals a little wider, though.
The verdict uses the main goal alone. When the winner converts worse on a secondary goal, the verdict adds a line like this one, and the list of experiments notes "but worse on signup":
But it converts worse on signup: −33.3% (−50% to −13%). Weigh that before shipping it.You can add, change, or remove secondary goals without restarting the experiment. Results are worked out from the recorded events, so a goal you add later has results for every visitor the experiment already counts.
Revenue#
An experiment usually compares how many visitors convert. With metric="revenue", it compares how much they bring in instead: the values sent with its goal, summed per visitor, over every visitor, buyers or not.
import { Experiment, Page, Variant } from "mockly"; export default function Pricing() { return ( <Page title="Pricing · Acme"> <Experiment name="plans" goal="purchase" metric="revenue"> <Variant name="control"> <h1>Simple pricing</h1> </Variant> <Variant name="annual"> <h1>Save 20% with yearly billing</h1> </Variant> </Experiment> <a href="/checkout" track="purchase"> Buy </a> </Page> );}Revenue per visitor catches what the conversion rate misses: a variant that sells to fewer visitors but sells them more. Each visitor's revenue counts up to the 99th percentile of buyers' once 100 visitors have bought, so a single huge order can't decide the experiment. See Reading results.
Guardrails#
A guardrail is a goal the experiment mustn't hurt. List them next to the goal, and a variant that significantly hurts one can't win:
import { Experiment, Page, Variant } from "mockly"; export default function Checkout() { return ( <Page title="Checkout · Acme"> <Experiment name="upsell" goal="add-on" guardrails={{ purchase: "drop", "refund-request": "rise" }}> <Variant name="control"> <p>Your order</p> </Variant> <Variant name="upsell"> <p>Add priority support for 9.99</p> </Variant> </Experiment> <a href="/addon" track="add-on"> Add it </a> <a href="/pay" track="purchase"> Pay </a> <a href="/refunds" track="refund-request"> Ask for a refund </a> </Page> );}"drop"means its rate mustn't fall, as for purchases;"rise"means it mustn't climb, as for refund requests. A list,guardrails={["purchase"]}, means"drop"for each.- Up to five, each once, and neither the goal nor a secondary goal (MK4023). Each should be recorded somewhere (MK4010).
- Experiments that share a name list the same guardrails (MK4017).
Guardrails are compared the way secondary goals are, with their own adjustment for every variant and guardrail, so the chance of stopping an experiment by mistake stays under 5%. When a variant hurts one, the verdict says so in place of any winner, the studio emails your notification list, and if the experiment's card says so, the experiment pauses. Like secondary goals, guardrails can change without restarting the experiment.
Goals that nothing records#
If an experiment's goal, or one of its secondary goals or guardrails, isn't recorded anywhere in the site, the compiler warns with MK4010:
Goal "signup" is never tracked anywhere in the site.It's a warning, so you can still publish, but the experiment can't count any conversions until something records the goal. A track attribute, a track() call, a form, and a page's goal all count. See Diagnostics for the full list of codes.
Trying goals in the preview#
In the studio's preview, clicking a tracked element shows "Goal fired: signup", and so does arriving at a page with a goal. Submitting a form shows "Form submitted: signup". Nothing is recorded, so you can click as much as you like.