A new DataFast install tends to go one of two ways. Nobody adds a goal, so the dashboard can say where visitors came from but not what they did. Or someone adds forty, and the monthly event quota is gone by the 12th.
The fix is knowing what each one is for.
Page views: already done
The script records a page view on load, and on every client-side navigation:
it listens to history.pushState and popstate. That covers Next.js App
Router links. You do not need a usePathname effect, and adding one double
counts.
So these need no code at all:
- "How many people see the pricing page?" Filter by page.
- "Where do visitors who read the docs come from?" Filter by page, then look at referrers.
- "Which landing page converts?" Revenue by entry page. That is the point of the product.
Goals: things a URL cannot tell you
A goal is an action on a page, or an outcome on the server:
| Question | Page view or goal |
|---|---|
| Did they open pricing? | Page view |
| Did they click "Start trial" on pricing? | Goal, browser: checkout_started |
| Did they create an account? | Goal, server: signup_completed |
| Did they finish onboarding step 3? | Goal, server: onboarding_step_completed with step: 3 |
| Did they pay? | Neither. DataFast reads payments from your provider |
That last row matters. Revenue comes from the connected payment provider, with
attribution from checkout metadata. A purchase_completed goal with an amount
is a second, worse copy.
Name goals like records
object_verb, past tense, snake_case:
checkout_started
signup_completed
project_created
newsletter_subscribed
DataFast accepts lowercase letters, digits, _, - and :, up to 64
characters. That is permission, not a convention. Pick one and enforce it, or
in a year you have signup, sign-up and signup:done as three series.
Never put a value in the name. plan_pro_bought and plan_team_bought are one
goal with a plan property. Otherwise every new plan breaks every funnel.
Stay away from DataFast's reserved names: payment, free_trial,
trial_started, trial_converted, the subscription_* family and
identify. It sends those itself once a payment provider is connected.
Make the catalogue a type
export interface GoalCatalogue {
checkout_started: { plan: string; interval: "month" | "year" };
signup_completed: { method: "email" | "oauth" };
}
export function trackGoal<N extends keyof GoalCatalogue>(
name: N,
properties: GoalCatalogue[N],
): void {
window.datafast?.(name, toGoalMetadata(properties));
}
Now trackGoal("signup") does not compile. The second spelling of a goal is a
type error in the editor, not a mystery series in the dashboard.
Properties: 10 strings, no people
DataFast keeps at most 10 parameters per goal, keys in lowercase, values up to 255 characters, all stored as strings. Use them for things you will filter by: plan, source, step, method.
DataFast's own examples pass email and name as goal parameters. Do not.
Goal parameters show up for everyone with dashboard access, forever. If you
need to find a person, use identify with your database user id, and keep the
email in your database.
Where to fire it
- Browser for intent: clicks, forms opened, a screen seen. Some of it will be lost to blockers and closed tabs, and that is fine for intent.
- Server for facts: an account row, a saved step. Use the Goals API after the write. It is complete, and a user cannot fake it from DevTools.
Never both for the same name. Two copies of one event double the count, and nothing can tell them apart later.
A budget, not a wishlist
Every goal is billed as an event. Before adding one, write the question it answers in one sentence. "Which channels bring people who finish onboarding?" earns a goal. "Might be useful later" does not.