Guide · Traffic and revenue

Which post brought paying customers?

To know which post made money, you need two links: one from the post to the visit (a UTM tag or the referrer), and one from the visit to the payment (a visitor id passed into checkout). With both in place, revenue can be counted per source, not just visitors.

9 min read

The otter celebrating a first sale

Visitors aren’t customers

You post the launch on X, Reddit and Product Hunt on the same day, mention it in your newsletter, and your blog keeps ticking along in search. Traffic jumps. Two weeks later you have eleven new paying customers. Which post brought them?

Most analytics tools answer a different question: which post brought visitors. Those are often not the same.

  • A Reddit thread can send 4,000 curious people who bounce.
  • A newsletter with 600 readers can send 40 visitors and 5 customers.

If you only look at visitors, you spend next month’s time on the wrong channel.

Revenue by source closes that gap. The idea is old (ad platforms have done it for years with heavy tracking), but you can do it for an indie app with no cookies and one extra line in your checkout code. The rest of this guide shows how, with notes on the traps that make the numbers lie.

1. Know where each visit came from

Referrers: free, but leaky

When someone clicks a link, the browser usually tells the next site where they came from (the referrer). Modern browsers send only the origin across sites, like https://www.reddit.com/, not the full thread URL.

That’s enough to say “Reddit”, not enough to say which post.

Referrers also go missing often. Many mobile apps (X, Reddit, LinkedIn, Slack, Discord), email clients and in-app browsers send none at all. Those visits land in “Direct”, which really means “we don’t know”.

UTM tags: one line per post

UTM parameters are query strings you add to the links you share. The three that matter:

  • utm_source: where the link lives (x, reddit, producthunt, newsletter).
  • utm_medium: the kind of traffic (social, email, cpc for paid).
  • utm_campaign: the specific push (launch-nov, r-sideproject).
one link per place you post
https://yourapp.com/?utm_source=x&utm_medium=social&utm_campaign=launch-nov
https://yourapp.com/?utm_source=reddit&utm_medium=social&utm_campaign=r-sideproject
https://yourapp.com/?utm_source=producthunt&utm_medium=social&utm_campaign=launch-nov
https://yourapp.com/?utm_source=newsletter&utm_medium=email&utm_campaign=issue-42

Keep a short list of source names and reuse them exactly. “X”, “x”, “twitter” and “x.com” become four rows in most tools. (BugOtter lowercases utm_source and turns t.co and twitter.com referrers into x.com, but it can’t guess that “tw” meant X.)

How BugOtter decides a visit’s source

For each visit, BugOtter takes the first utm_source it sees (it also reads ref and via, which some directories and affiliate links use), and falls back to the referrer’s domain when there’s no tag. A visit with neither is Direct.

Each source is then put in a channel:

  • Paid: a medium like cpc or paid.
  • Email: a medium like email or newsletter, or sources like Substack and Beehiiv.
  • AI assistants: ChatGPT, Perplexity, Claude and others.
  • Search, Social, Referral or Direct for the rest.

For search, the referrer says “Google” but not the query. If you want the queries and the revenue each landing page brought, connect Google Search Console from the site’s Analytics page; it’s read-only.

3. Provider by provider

Each payment provider stores checkout data a little differently, so what gets linked differs. As of Nov 4, 2026:

ProviderWhat you doWhat gets linked
Stripe Payment LinksNothing. The script adds the visit id as client_reference_id to every buy.stripe.com link on your page that doesn’t already have one.One-time and subscription payments.
Stripe Checkout or SubscriptionsSet metadata.bugotter_visitor_id when you create the Checkout Session or the Subscription.One-time Checkout payments, subscriptions and their renewals. A bare PaymentIntent with no invoice isn't counted yet.
PolarSet metadata.bugotter_visitor_id on the checkout.Orders, subscriptions and their renewals.
CreemSet metadata.bugotter_visitor_id on the checkout (POST /v1/checkouts).Subscriptions and their renewals. One-time payments aren’t linked yet.
Lemon SqueezyNothing to do.Not linked to sources yet: its API doesn’t expose checkout data BugOtter can read back. Revenue still shows per app and product.

BugOtter only reads from these providers, with a restricted or read-only key where the provider offers one. It never charges, refunds or emails your customers.

4. Read the Sources view

On a site’s Analytics page, the first card has three tabs:

  • Channels: Social, Search, Email…
  • Sources: x.com, reddit, producthunt, newsletter…
  • Campaigns: your utm_campaign values.

When revenue is connected, the rows rank by revenue by default, with visitors as the tie-break. You can switch the ranking to your top goal (say, signups) or to visitors.

BugOtter’s Analytics cards: Channels with visitors, revenue and error rate per channel, a red insight about referral visitors, then Pages and Countries
  1. Channels, Sources, Campaigns. Three ways to slice where visits came from.
  2. Revenue per row. Social brought $175, Referral $29, next to visitors and error rate.
  3. Error insight. Referral visitors hit an error 6× more often than average, with a link to the error.
The Analytics cards on the live demo. Click a row and the whole page filters to it.

Three habits make this view useful:

  • Switch the chart metric to “Revenue / visitor”. It shows which source sends people who buy, not just many people. A small newsletter often wins here.
  • Click a row to filter everything. Click reddit and the whole page (pages, countries, goals, revenue) shows only Reddit visits. The chart says “revenue linked to these visits only” so you don’t mistake it for the total.
  • Compare periods. Turn on compare to set this launch against the last one, row by row.

If one source’s visitors hit errors three times more often than the site as a whole, a line under the card says so and links to the error. It happens more than you’d think: a Product Hunt crowd on old Safari, a Reddit app’s in-app browser, a country your payment form breaks in.

The same split on the Revenue page, and for your agent

The Revenue page has the same split as a slice (“By source”) next to by product and by country.

Your coding agent can ask for it too: get_analytics with breakdown=sources and rank=revenue, or get_revenue, whose detail includes which sources bring customers who stay.

BugOtter’s Revenue page: MRR of $1,389, a waterfall of how MRR moved in the last 30 days, and a forecast
  1. MRR across your connected providers, with the change since the start of the range.
  2. How MRR moved: new, upgrades, downgrades and cancellations as a waterfall.
  3. Forecast if the last 30 days repeat. The By source slice sits further down the page.
The Revenue page on the live demo. Sources, products and countries are slices of the same revenue.

Pitfalls that skew the answer

First touch, last touch, and what “brought” means

A customer might find you on Reddit, forget, see you on X a week later and buy. Did Reddit or X bring them? A last-touch model says X; a first-touch model says Reddit. Neither is the truth; they answer different questions (“what closes?” vs “what introduces?”).

BugOtter credits a payment to the visitor’s first visit in the range you’re viewing.

  • Default per-visit id: the id only spans one visit, so in practice that’s the visit where they bought.
  • Cookie mode: the id spans a year, so it’s the earliest visit in your date range. Widen the range and credit can move to an earlier source.

Know which one you’re reading before you act on it.

20–40%

Ad blockers hide 20–40% of visits

Blocklists often include analytics scripts, and the people who use blockers (developers, for one) may be exactly your buyers. A missing visit can’t be linked to a payment, so blocked buyers show as unattributed revenue.

Serve the script from your own domain and blockers can’t tell it apart from your app. In Next.js that’s three rewrites; BugOtter’s Traffic tab has them with your app’s proxy key filled in, plus versions for vercel.json and Netlify.

next.config.ts
async rewrites() {
  return [
    { source: "/_bo/bo.js", destination: "https://bugotter.com/bo.js" },
    { source: "/_bo/api/collect", destination: "https://bugotter.com/api/collect?via=YOUR_PROXY_KEY" },
    { source: "/_bo/api/errors", destination: "https://bugotter.com/api/errors?via=YOUR_PROXY_KEY" },
  ];
}
// then: <script defer src="/_bo/bo.js" data-app="YOUR_APP_ID"></script>

More traps

  • UTMs on your own links. Never tag internal links or your own nav. On a new visit, the tag becomes the source and wipes the real one.
  • Checkout on another domain. Hosted checkouts are fine, because the id travels in metadata, not in a cookie. What breaks it is creating the checkout without passing the id.
  • Your own test purchases. Open your site with #bo-ignore at the end of the address once per browser you test with, so your visits don’t count.
  • Tiny numbers. Three customers from Reddit and one from X isn’t a trend. Read a few launches together before you drop a channel.
  • Unattributed revenue. Payments with no visit id (Lemon Squeezy, older customers, blocked visits) aren’t in the Sources card. Compare the attributed total with the Revenue page so you know how much of the picture you see.

Lemon Squeezy and Creem behaviour checked in BugOtter’s sync code on Nov 4, 2026. Referrer behaviour per app varies and changes; tag the links you control.

Questions

Do I need cookies to attribute revenue?

No. By default BugOtter keeps a random visit id in sessionStorage, which ends when the tab closes, so a purchase in the same visit links to where that visit came from. Cookies only help when people come back days later to buy; that’s the optional data-attribution="cookie" mode.

Why does Reddit or X show as Direct?

Their mobile apps, many email clients and some in-app browsers send no referrer. Without a referrer or a UTM tag, a visit has no source, so it counts as Direct. Tag the links you post with utm_source and the source survives.

Can I attribute Lemon Squeezy sales to sources?

Not yet. Lemon Squeezy’s API doesn’t expose checkout custom data in a way BugOtter can read back, so its revenue shows per app and product but not per source.

Is this first-touch or last-touch?

BugOtter credits a payment to the visitor’s first visit in the date range you’re looking at. With the default per-visit id, that’s usually the visit where they bought. With cookie mode, it’s the earliest visit in the range from that browser.

Does it count refunds?

Yes. Revenue by source is paid amounts minus refunds, per currency. Currencies are never added together.

See which post made money, not just traffic.

14 days of everything, no card needed. Plans from $10 a month, billed yearly.