Do You Actually Know How Your Marketing Campaign Is Performing?
You spent €200 on a LinkedIn push. The ad dashboard says 4,300 impressions, 87 clicks, €2.30 CPC. Good campaign? Bad campaign? You have no idea — and the dashboard can't tell you, because it stops at the click. Here is how to measure marketing campaign performance end-to-end with one funnel and five custom events, so the next campaign question has a real answer.
Why impressions and clicks don't tell you if the campaign worked
Ad platforms optimise for the metrics they can see. They can see impressions, clicks, and (sometimes) a pixel firing on a thank-you page. What they can't see is whether the 87 people who clicked your ad actually became users — let alone paid ones.
That gap is the whole game. A campaign with 87 clicks and 4 paid conversions is wildly different from a campaign with 870 clicks and 0 paid conversions. The ad dashboard will rank the second one higher on every metric it shows you. It will also be the one quietly burning your runway.
The fix is not a bigger analytics stack. The fix is to extend the funnel one step at a time, on your own domain, until it reaches the action that pays your bills.
The campaign funnel: five steps, five events
Take Inboxly, the small SaaS we've been using as a running example. This week it launched a new "team inbox" feature with a €200 LinkedIn ads campaign. The funnel we care about looks like this:
ad click → landing view → signup started → signup completed → paid upgrade
Each arrow is a potential drop-off. Each step is one custom event, fired the moment it happens. The ad dashboard owns the first arrow. We own the other four — and the other four are where the campaign actually succeeds or fails.
Step 1 — Tag the click on your own domain
Don't rely on the ad platform's UTM tagging alone; UTMs disappear on the second navigation. Instead, route the ad through a thin endpoint on your domain that stamps the campaign and forwards on:
// /go/li-team-inbox → Cloudflare Worker
export async function handleAdClick(req) {
return Response.redirect(
'https://inboxly.app/team-inbox?c=li_team_inbox',
302
);
}
The c=li_team_inbox param is the campaign tag — short, lowercase, channel-prefixed (li_ for LinkedIn, gads_ for Google, nl_ for newsletter). One prefix per channel keeps the events list tidy when you run several campaigns in parallel.
Step 2 — Fire campaign_click on landing
On the landing page, read the campaign tag and fire one event per campaign:
const c = new URLSearchParams(location.search).get('c');
if (c) {
window.logly('event', 'campaign_click_' + c);
}
Each campaign gets its own event name (campaign_click_li_team_inbox). In the Logly events panel they group naturally — alphabetised by prefix — so you can compare LinkedIn vs Google vs newsletter at a glance.
Step 3 — signup_started and signup_completed
These two are the workhorse pair from the events post. Fire signup_started when the form opens or first focuses, signup_completed when the API confirms account creation. The ratio between them is your form's drop-off — the same ratio for every campaign, but the absolute count per campaign is what tells you whether the channel actually feeds the form.
// when the form is first focused or opened
window.logly('event', 'signup_started');
// after the API confirms account creation
window.logly('event', 'signup_completed');
Step 4 — paid_upgraded
The only metric that pays your bills. Fire it server-side from your billing webhook (Stripe, Paddle, LemonSqueezy) so a refund or a failed first charge doesn't inflate it:
// Stripe webhook handler, on invoice.payment_succeeded (first invoice only)
if (invoice.billing_reason === 'subscription_create') {
await sendLoglyEvent('paid_upgraded');
}
Server-side events use the same window.logly('event', name) shape via a tiny HTTP call from your Worker. The full payload is in the docs.
Reading the campaign funnel honestly
A week after the LinkedIn push goes live, you open the Logly funnel view and stack the five events for campaign tag li_team_inbox. You get something like this:
campaign_click_li_team_inbox 87
landing_team_inbox_viewed 82 (94%)
signup_started 19 (23%)
signup_completed 11 (58%)
paid_upgraded 3 (27%)
The bottom number is 3 paid users from €200 — €66 per paid user. Whether that's good depends on your LTV; for an Inboxly-sized SaaS at €19/month with 14-month average lifetime, it's profitable. But the bottom number is a vanity number on its own. The interesting reading lives in the deltas:
- 87 → 82 (94%) — page load is fine. Don't optimise it.
- 82 → 19 (23%) — three quarters of clickers bounce without trying to sign up. The landing page is the leak. Hero copy or CTA placement, not the ad.
- 19 → 11 (58%) — form drop-off is normal. Acceptable.
- 11 → 3 (27%) — only a quarter of signups upgrade. This is the trial experience, not the campaign. The campaign delivered qualified intent; onboarding is what's leaking.
Notice that none of those four observations is "spend more on LinkedIn" or "spend less on LinkedIn". The ad did its job — it brought 87 reasonably qualified people to a page about the new feature. The leaks are downstream, on surfaces you own and can change without giving any money to LinkedIn.
Treat the campaign tag as the single join key. li_team_inbox appears in the URL, in the event name, and (if you want) in your own DB row for the signup. Anywhere you need to ask "what did this campaign actually do," you have one string to grep.
Running several campaigns side by side
The same week, Inboxly also sent a newsletter blast (nl_team_inbox) and posted on LinkedIn organically with a tagged link (li_organic_team_inbox). After 7 days the comparison looks like:
click signup_completed paid_upgraded
li_team_inbox (paid ads) 87 11 3
nl_team_inbox (newsletter) 142 38 12
li_organic_team_inbox 54 7 2
The newsletter wins on every metric, at zero marginal cost. Paid LinkedIn produces three customers for €200. Organic LinkedIn matches paid on conversion rate but at a fifth of the reach. The action items write themselves: keep doing newsletter pushes for launches, keep posting organically, and either kill the paid LinkedIn channel or change the creative — what you absolutely should not do is what the ad dashboard would suggest, which is "increase budget on the campaign with the lowest CPC".
What to do with this every Friday
The discipline part is the same as outreach attribution: schedule fifteen minutes on Friday morning to look at the funnel for every campaign tagged in the previous two weeks. Three decisions per campaign:
- Kill — bottom-of-funnel conversions ≤ what you'd expect from organic baseline. Stop the spend, archive the creative, move on.
- Iterate — top of funnel is healthy (clicks and landing views), bottom is leaking. The campaign is fine; fix the landing page or the trial experience.
- Scale — entire funnel converts at or above your blended average. Double the budget on the same creative; don't change anything else yet.
The reason most marketing budgets quietly evaporate is that nobody ever makes any of these three decisions explicitly. They make them by inertia — letting last quarter's spend roll over, optimising for whatever number is biggest on the ad dashboard, never connecting the click to the paid user. A campaign funnel built from five events fixes that with about an hour of setup per channel and fifteen minutes a week of reading.
Variations worth knowing
Multi-touch campaigns. If a prospect clicks the ad, leaves, then comes back from a newsletter a week later and converts — that's two campaign clicks for one signup. Store the campaign tag on the first touch and keep it; don't overwrite on the second click. The COALESCE pattern from the outreach post works identically here.
Cohort retention. Once you have paid_upgraded tagged per campaign, you can ask the more honest question three months later: of the three paid users from li_team_inbox, how many are still paying? A campaign that produces three customers who all churn in month two is worse than a campaign that produces one customer who stays for a year.
Don't track view-through conversions. Ad platforms love them; they're mostly an accounting trick to claim credit for conversions that would have happened anyway. Stick to click-through conversions — the events you fire on your own domain are inherently click-through, which is a feature, not a limitation.
Where this fits with the rest of the stack
This is the natural endpoint of the small-SaaS analytics arc we've been building post by post. Custom events name the moments that matter. Funnels string them into a sequence. Campaign tags partition that sequence by source, so you can answer the only marketing question that actually matters: which channel produces customers that pay and stay?
Five events. One funnel. Real campaign answers.
Logly's custom events and funnel view are part of every plan, including the free tier. Full event API in the docs.
Get started free →