SaaS analytics guide · ~10 minute setup · Updated May 31, 2026 · By Admaxxer Team

SaaS analytics setup — identify users, fire funnel events, and attribute MRR

Installed the pixel and connected Stripe, but your funnel and channel attribution are empty? Two steps close the loop: identify users on signup and fire custom funnel events. Once those are wired, every trial, subscription, and dollar of recurring revenue traces back to the channel that earned it — so you get trial-to-paid by channel, MRR by source, and CAC payback by cohort.

Why pageviews + Stripe alone leave your funnel empty

Pageviews tell you traffic. Stripe tells you revenue. Neither one knows which paying customer came from which channel — that link is created when you call identify(user.email) on signup and fire a named event at each funnel step. On a framework (Next.js, Remix, SvelteKit), use the SSR-safe @admaxxer/pixel npm SDK so these calls never crash during server render. A SaaS funnel is signup → trial → subscription → recurring revenue; this guide instruments exactly those steps. (If you run an online store instead, you want the products-and-orders path, not this one — the two are intentionally separate.)

Four steps: install, identify, events, Stripe

Each step is independent — ship them in any order and verify each one on its own. Estimated total: 10 minutes.

1. Install the pixel

One snippet in your site’s <head> starts capturing pageviews and the anonymous visitor journey. The Pro variant (script.plus.js) also auto-captures form submits and outbound clicks — handy because most SaaS signups are form-driven.

<script defer
  data-website-id="YOUR_WEBSITE_ID"
  data-domain="yourdomain.com"
  src="https://admaxxer.com/js/script.plus.js"></script>

Find YOUR_WEBSITE_ID and your domain at /dashboard/pixel. Using Next.js, Remix, SvelteKit, or SolidStart? The npm package is SSR-safe.

2. Identify users on signup

Call identify(user.email) the moment a user signs up or logs in. This is the step that links the anonymous visitor who clicked your ad to the paying customer in your billing system — so every future subscription and dollar of recurring revenue is attributed back to the channel, campaign, and source that originally acquired them. Most SaaS apps are SSR frameworks (Next.js, Remix, SvelteKit), so use the SSR-safe npm SDK — calling window.admaxxer.identify(…) directly from a server-rendered component would crash with window is not defined.

// RECOMMENDED for SaaS frameworks — the SSR-safe npm SDK. identify() and
// track() no-op during server render and buffer until the pixel loads, so
// you can call them anywhere with NO "typeof window" guards.
import { initPixel, identify, track } from '@admaxxer/pixel';

initPixel({ websiteId: 'YOUR_WEBSITE_ID' });

// Simplest form — pass the email you already have at signup:
identify(user.email);

// Richer form — pass your stable user id plus traits:
identify(user.id, {
  email: user.email,
  plan: user.plan,
  signed_up_at: user.createdAt,
});

// Plain / non-framework sites use the hosted <script>'s window.admaxxer
// global instead — SSR-guard it so a server render can't crash:
if (typeof window !== 'undefined') {
  window.admaxxer.identify(user.email);
}

Why the npm SDK: its identify()/track() are SSR-safe (no-op on the server, buffered until the pixel loads), so they're callable anywhere with no guards. The call is idempotent, so it’s safe to run on every authenticated page load — the original ad-click chain then stays attached through trial, conversion, and churn, across devices and months later. The hosted-script global is window.admaxxer; the legacy window.admx global does NOT exist.

3. Fire custom funnel events

Send a named event at each step of your funnel with track('event_name', { ...props }) from the SSR-safe npm SDK. These become goals you can filter and attribute by channel — so you see which sources produce real trial starts and paid subscriptions, not just pageviews. Always fire Signup yourself. For Trial Started and Trial Converted: if your trial requires a card, Stripe fires both automatically — skip them; if it’s a no-card / app-side trial, fire the same canonical names yourself so your trials land in the same funnel tiles.

import { track } from '@admaxxer/pixel';

// ALWAYS fire this yourself — there is no Stripe equivalent (pre-payment):
track('Signup');

// Trial + conversion — who fires these depends on your trial type:
//   • Card-required trial → Stripe fires 'Trial Started' / 'Trial Converted'
//     AUTOMATICALLY. Do NOT also fire them here (you'd double-count). Skip them.
//   • No-card / app-side trial (Stripe never sees the trial) → fire the SAME
//     canonical names yourself, so trials land in the same funnel tiles:

// When a free trial begins:
track('Trial Started', { plan: 'pro' });

// When the trial converts to a paid subscription:
track('Trial Converted', { plan: 'pro', mrr: 49 });

// Any extra milestone that matters to your funnel (always pixel-fired):
track('Demo Booked', { source: 'pricing_page' });
track('Activated', { milestone: 'first_project_created' });

// Plain / non-framework sites fire events by calling the window.admaxxer
// global directly — SSR-guard it. (No-card trial only; for a card trial let
// Stripe fire 'Trial Started'.)
if (typeof window !== 'undefined') {
  window.admaxxer('Trial Started', { plan: 'pro' });
}

Pass any plain-object properties you want to segment by later (plan, MRR, source, milestone). Prefer zero-JS? Add data-admx-goal="Trial Started" to a button and the event fires on click — no handler code. (Card-required trial? Let Stripe fire Trial Started/Trial Converted instead, so you don’t double-count.)

4. Connect Stripe for MRR

Paste a Stripe restricted key (rk_live_…) in /integrations and recurring revenue flows in automatically — trial starts, paid conversions, and cancellations all stitch back to the original visitor. No webhook handler to write on your side.

// Recommended: tie revenue to the original visitor across devices by
// passing the visitor id into your Stripe Checkout Session.

// Client-side — read the id the pixel set:
const visitorId = window.admaxxer.getVisitorId();

// Server-side — attach it as Checkout Session metadata:
const session = await stripe.checkout.sessions.create({
  // ...your existing config
  metadata: { admx_visitor_id: req.cookies.admx_visitor_id || '' },
});

Use a restricted key (read-only scopes), never a secret key. Without the visitor-id metadata, revenue still stitches via the email you passed to identify() — slightly less precise if a customer signs up and pays with different emails. Full walkthrough: Stripe revenue connector.

What shows up where

Once the four steps are wired, each funnel stage is attributed to the source that produced it:

All of this lands in Marketing Acquisition, where you can filter by any funnel goal and split it by channel and source.

Revenue & MRR accuracy

Once Stripe is connected, your recurring revenue rolls up automatically — sourced directly from Stripe, your system of record, and continuously reconciled against your live Stripe data. Here is exactly what each number means, and how a live “Matches Stripe” indicator keeps it honest even when your billing changes quietly in the background.

What MRR (Monthly Recurring Revenue) is

MRR is the predictable revenue your active subscriptions bill every month, normalized to a monthly figure. Annual plans are divided by 12 and quarterly plans by 3, so one number always means “per month.” For example, 20 customers on a $49/mo plan plus 5 on a $490/yr plan is 20 × $49 + 5 × ($490 ÷ 12) = $980 + $204.17 = $1,184.17 MRR.

ARR (Annual Recurring Revenue)

ARR is your annualized run-rate — simply MRR × 12. It answers “what does this book of subscriptions earn over a year if nothing changes,” and it moves in lock-step with MRR because it is derived from the same Stripe-sourced number. Example: $1,184.17 MRR is $14,210 ARR; if a discount expires and MRR rises to $1,234, ARR updates to $14,808 automatically.

Active (paying) subscribers

This is the count of subscriptions Stripe currently marks active and billing — the people actually paying you right now. Trials that haven’t converted, cancelled subscriptions, and fully-comped accounts that bill $0 are reported distinctly, so the headline count is never inflated. Example: 120 active subscriptions, 30 in-trial, and 4 fully-comped — the active-paying figure is 120 (or 124 if you choose to count comps as active seats), and the split is always shown, so the number means exactly what Stripe’s customer list shows.

Gross vs net MRR

Gross MRR is the full list price of every active plan. Net MRR subtracts any active recurring discount — a coupon, a comp, or a promo that recurs every billing cycle. Net is the cash you can actually count on, so we headline Net to match what Stripe’s own subscription totals reflect. Example: a $99/mo plan with a forever 50%-off coupon counts as $99 gross but $49.50 net; a fully-comped (100%-off) subscriber counts as $0 MRR, exactly as Stripe’s dashboard shows. A one-time (first-month-only) discount changes neither gross nor net, because it isn’t recurring.

Trial → paid conversion

This is the share of trials that turn into paying subscriptions, split by the channel and source that first acquired the visitor — so you see which traffic produces trials that actually convert, not just the cheapest signups. Example: 200 trials start and 64 convert to paid = 32% trial-to-paid; filtered by source you might find paid search converts at 41% while a cheaper social channel converts at 12% — the real basis for where to spend.

MRR movement: new, expansion, contraction, churn, reactivation

Every change to MRR is bucketed so you can see why it moved: New (a first paid subscription), Expansion (an existing customer upgrades or adds seats), Contraction (a downgrade or dropped seats), Churn (a cancellation, whether voluntary or from a failed payment), and Reactivation (a churned customer returning). Example: in a month you add $600 New + $150 Expansion + $40 Reactivation − $40 Contraction − $90 Churn = +$660 net new MRR. The waterfall shows each bucket so a flat month is never mistaken for a quiet one, and churn ties back to the channel that originally acquired the customer.

Voluntary vs involuntary churn

Churn is split by cause. Voluntary churn is a customer who chose to cancel. Involuntary churn is a subscription lost to a payment that kept failing — an expired card or insufficient funds — recoverable revenue you’d otherwise never notice leaking. Example: $90 churn in a month breaks down as $60 voluntary + $30 involuntary; the $30 involuntary is a dunning problem, not a product problem, and it’s the bucket you can often win back.

Cash collected (and why it differs from MRR)

Cash collected is the actual money your billing provider charged and collected in a period — including annual prepayments, one-time fees, and proration. It is deliberately different from MRR, which is a smoothed monthly run-rate, so the two rarely match and that is expected. Example: a customer prepays $1,188 for an annual plan in January — cash collected that month is $1,188, but they only add $99 to MRR. Both are correct; they answer different questions.

A live “Matches Stripe” indicator on your MRR card

Your MRR, ARR, active subscribers, and churn are sourced directly from Stripe — your system of record — and continuously reconciled against your live Stripe data. The MRR card carries a live badge that confirms the two agree: it reads “Matches Stripe” whenever the figures are equal. When anything diverges — say a recurring discount expired and the run-rate shifted — the badge becomes a clear, explained delta (for example, “+$49 vs Stripe — discount expired”) instead of silently drifting, so a reporting cycle never burns on a mismatch you didn’t know about. You don’t have to do anything: your numbers stay in step with what Stripe would show you, on their own, and the badge tells you so at a glance.

How reconciliation works

Admaxxer reads your live subscription state from Stripe and rebuilds each metric from it, continuously — so the figure on screen always reflects your real, current Stripe data, not a snapshot that can go stale. Two everyday cases show why that matters. First, a 100%-off (fully-comped) subscriber counts as $0 MRR — exactly as Stripe’s own dashboard shows, never the list price — so your headline MRR is never inflated by free accounts. Second, when a recurring discount ends and a subscriber’s real price rises, the change is picked up automatically: your Net MRR lifts the moment the coupon expires, with no new sale and nothing for you to do. The result is a number that always equals what your billing provider would show you, confirmed live by the “Matches Stripe” badge.

Why your MRR might change

SaaS metrics vs ecommerce metrics

Admaxxer shows the vocabulary that fits your business model and never conflates the two. A subscription business and an online store measure success with completely different numbers, so a SaaS dashboard never shows orders and an ecommerce dashboard never shows MRR.

This guide is the SaaS path — identify on signup, fire funnel events, connect Stripe for MRR. Ecommerce concepts like orders, “charges,” or a per-order “% attributed” figure never appear as SaaS metrics. Running an online store instead? See the ecommerce revenue path.

Set it up with your AI agent

Already coding with an AI assistant? Connect Claude, Cursor, or any MCP client to Admaxxer, then ask it to call the admaxxer_get_event_setup tool. It returns the exact install, identify, and funnel-event snippets above — pre-filled with your real website id — so your agent can drop them straight into your app. No hunting for IDs, no manual copy-paste.

  1. Connect your AI — generate a token and paste it into your AI client’s MCP config (about two minutes). See Connect any AI.
  2. Ask for the setup — prompt your agent: “Fetch my Admaxxer event setup and wire it into this app.”
  3. Agent wires it in — it receives the snippets pre-filled with your website id and adds them for you.

The admaxxer_get_event_setup tool is read-only — it returns code text only and changes nothing in your account. Browse every supported AI client and the full tool list at Connect any AI.

Verify it works

  1. Open your live site in a fresh tab. Within ~5 seconds a Pageview should appear in Pixel › Realtime.
  2. Sign up with a test account. Your signup goal and the identify call should both register within ~5 seconds.
  3. Fire a test funnel event — e.g. track('Trial Started', { plan: 'pro' }) — and confirm it appears as a goal. (No-card trials only — a card-required trial fires this via Stripe.)
  4. Run a $1 test subscription through Stripe Checkout. New recurring revenue should attribute to your test visitor’s original source.
  5. Open Marketing Acquisition and filter by your funnel goal — you should see it split by channel and source.

FAQ

I already have pixel pageviews and Stripe MRR — why is my funnel empty?

Pageviews and revenue alone can’t tell which paying customer came from which channel. The link is the identify() call on signup plus the custom funnel events. With the SSR-safe npm SDK, call identify(user.email) when a user signs up and always fire track('Signup'). If your trial requires a card, Stripe fires Trial Started and Trial Converted automatically — don’t also fire them from the pixel. If your trial is no-card / app-side, fire track('Trial Started', { plan }) and track('Trial Converted', { plan, mrr }) yourself. (On a plain HTML site, window.admaxxer.identify(…) and window.admaxxer('Trial Started', …) work too, wrapped in a typeof window check.) Once those are in place, trial-to-paid conversion by source, MRR by channel, and CAC payback populate automatically.

What exactly does identify(userEmail) do?

It links the anonymous visitor in the current browser to the named user. Every event that browser fired before signup — including the original ad click and its UTM/source — gets attached to that user. From then on, every trial, subscription, and dollar of recurring revenue is attributed back to the channel that acquired them, even across devices and months later.

I’m on Next.js / Remix / SvelteKit — how do I call identify and track safely?

Install the SSR-safe npm SDK with npm install @admaxxer/pixel, then import { initPixel, identify, track } from '@admaxxer/pixel'. Call initPixel({ websiteId }) once, then identify(user.email) and track('Trial Started', { plan }) anywhere — including server components or module top-level. The SDK no-ops during server render and buffers calls until the pixel loads, so you never need a typeof window guard and never hit window is not defined. Calling window.admaxxer.identify(…) directly in a server-rendered component would crash instead.

Should I pass the email or a user id to identify()?

Either works. The single-argument form identify(userEmail) is the quickest to ship. The richer form identify(user.id, { email, plan }) ties the visitor to your stable internal user id and lets you attach traits (plan, signup date) for segmenting later. If you have both, prefer the id form and include email in traits.

What event names should I use for a SaaS funnel?

For the core lifecycle, use the exact canonical names so pixel-fired and Stripe-fired events land in the same funnel tiles: Signup, Trial Started, and Trial Converted. Signup is always pixel-fired; Trial Started and Trial Converted are pixel-fired only for a no-card / app-side trial (a card-required trial gets them from Stripe automatically). Beyond those, name any extra milestone however you like — e.g. Activated, Demo Booked, Upgraded — and pass properties you want to segment by, such as { plan: 'pro', mrr: 49 }. Each custom name becomes a goal you can filter and attribute by channel.

Do I have to fire events myself if I connect Stripe?

Connecting Stripe with a restricted key brings in trial starts, paid conversions, cancellations, and MRR automatically — no handler code. Firing the client-side events yourself is still recommended for funnel steps Stripe never sees (demo booked, account activated, feature adopted) and for instant in-app confirmation. The two sources reinforce each other.

Does Admaxxer reconcile MRR with Stripe?

Yes. Stripe is the system of record, and your MRR, ARR, active subscriber count, and churn are sourced directly from your live Stripe data and continuously reconciled against it. The MRR card carries a live “Matches Stripe” indicator: when our figure equals what Stripe shows, it stays green; if anything diverges, the badge turns into a clear, explained delta instead of silently drifting — so a reporting cycle never burns on a mismatch you didn’t know about.

What is the “Matches Stripe” indicator on the MRR card?

It’s a live trust badge on your MRR. Because Admaxxer reconciles against your live Stripe data continuously, the badge reads “Matches Stripe” whenever the two agree. If they ever diverge — say a recurring discount expired and the run-rate shifted — the badge shows the exact delta and the reason rather than quietly changing the number. You always know whether the figure on screen equals Stripe, at a glance.

How does reconciliation work, in plain language?

Admaxxer reads your live subscription state from Stripe and rebuilds each metric from it, continuously. Two worked examples: a fully-comped subscriber counts as $0 MRR — exactly as Stripe’s own dashboard shows, never the list price; and an expired discount that raises a subscriber’s real price is picked up automatically, so your Net MRR rises the moment the coupon ends, with no new sale and nothing for you to do. The result is a number that always equals what your billing provider would show you.

How does Admaxxer calculate my MRR?

MRR is the predictable revenue from your active subscriptions, normalized to a monthly figure: annual plans are divided by 12 and quarterly by 3, so the number always means “per month.” We report MRR net of any active recurring discount — a forever or repeating coupon reduces it; a one-time first-month discount does not. For example, 20 customers on a $49/mo plan plus 5 on a $490/yr plan is $980 + $204.17 = $1,184.17 MRR. It is sourced directly from Stripe and continuously reconciled against your live data, with a “Matches Stripe” badge that confirms the two agree.

What’s the difference between gross and net MRR?

Gross MRR is the full list price of every active plan. Net MRR subtracts any discount that recurs every billing cycle, which is the revenue you can actually count on, so we headline Net. A $99/mo plan with a forever 50%-off coupon is $99 gross but $49.50 net. A fully-comped (100%-off) subscriber counts as $0 MRR, exactly as Stripe shows. A one-time discount affects neither, because it isn’t recurring.

What’s the difference between voluntary and involuntary churn?

Voluntary churn is a customer who chose to cancel. Involuntary churn is a subscription lost to a payment that kept failing — an expired card or insufficient funds — recoverable revenue you’d otherwise never notice leaking. Splitting the two tells you when a churn spike is a product problem (voluntary) versus a dunning problem (involuntary, often winnable back).

Why did my MRR change when I didn’t make a sale?

The most common silent cause is a recurring discount expiring: when a coupon naturally ends, the customer goes back to full price, so your Net MRR rises with no new subscription. Billing systems don’t always send a notification when a discount simply runs out, so Admaxxer reconciles your subscriptions with Stripe continuously and lifts (or lowers) the number automatically — and the “Matches Stripe” badge confirms the new figure still equals Stripe. Other everyday causes are upgrades and added seats (expansion, up), downgrades or a newly applied recurring coupon (contraction, down), reactivations (up), and cancellations or failed payments (churn, down).

Why doesn’t my cash collected match my MRR?

They answer different questions, so they rarely match — that’s expected. MRR is a smoothed monthly run-rate; cash collected is the actual money charged in a period, including annual prepayments, one-time fees, and proration. A customer who prepays $1,188 for an annual plan adds $1,188 to that month’s cash collected but only $99 to MRR. Both numbers are correct.

Are SaaS metrics and ecommerce metrics the same?

No, and Admaxxer never conflates them. A SaaS dashboard shows MRR, ARR, active subscribers, trial-to-paid conversion, and churn. An ecommerce dashboard shows orders, AOV (average order value), ROAS (return on ad spend), and LTV from one-off purchases. SaaS concepts like MRR never appear on an ecommerce dashboard, and ecommerce concepts like orders or a per-order “% attributed” figure never appear as SaaS metrics. This guide is the SaaS path — identify on signup, fire funnel events, connect Stripe for MRR.

Ready to close the loop?

Install the SSR-safe @admaxxer/pixel SDK, call identify(user.email) on your signup handler, and always fire track('Signup'). For a no-card / app-side trial, also add Trial Started and Trial Converted events with track() (a card-required trial gets those two from Stripe automatically). Then connect Stripe with a restricted key. Each step is independently verifiable in Pixel › Realtime. Within minutes your Marketing Acquisition dashboard shows trial-to-paid by channel, MRR by source, and churn-by-cohort.

Full SaaS install reference · Connect Stripe · Custom goals · All documentation