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.

Tag what you post
Autm_sourceon each link survives where referrers go missing (mobile apps, email, in-app browsers).Pass one id to checkout
Put the visit id on the checkout asbugotter_visitor_id. Renewals follow the first payment.Rank by revenue
Read sources by revenue and revenue per visitor, not visitors. A small newsletter often wins.
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,cpcfor paid).utm_campaign: the specific push (launch-nov,r-sideproject).
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-42Keep 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
cpcorpaid. - Email: a medium like
emailornewsletter, 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.
2. Link the visit to the payment
This is the half most people miss. Your analytics knows the visit came from Reddit. Your payment provider knows someone paid $29. Neither knows they were the same person, unless you pass something from one to the other.
BugOtter’s script gives every visit a random id (it looks like bo_ followed by hex). You read it in the browser and put it on the checkout as metadata.
When BugOtter syncs payments from your provider, it reads that metadata back and joins the payment to the visit. No names, no emails, nothing about the person.
- Visit idThe script gives the visit a random
bo_…id and notes its source. - Checkout metadataYour checkout carries
bugotter_visitor_id. - PaymentBugOtter syncs the payment, read-only, and reads the id back.
- SourceThe payment counts toward the visit’s source: x.com, reddit, newsletter…
Read the visit id in the browser
client, where you start checkout const visitorId = window.bo?.visitorId?.(); await fetch("/api/checkout", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ priceId, visitorId }), });Put it on the checkout as metadata
server, e.g. app/api/checkout/route.ts (Stripe) const { priceId, visitorId } = await req.json(); const valid = typeof visitorId === "string" && /^bo_[a-f0-9]{16,40}$/.test(visitorId); const session = await stripe.checkout.sessions.create({ mode: "subscription", line_items: [{ price: priceId, quantity: 1 }], success_url: "https://yourapp.com/thanks", ...(valid ? { metadata: { bugotter_visitor_id: visitorId } } : {}), });Only set it when it’s a real id: an empty value or anything else is ignored anyway, and checking keeps junk out of your provider. For Polar and Creem, the key is the same:
metadata: { bugotter_visitor_id: visitorId }on the checkout you create.Let renewals follow
You only tag the first checkout. A subscription’s renewals carry the link from the subscription it belongs to, so month three of a customer still counts toward the post that brought them.
Short visits vs long journeys
By default the visit id lives in sessionStorage: it lasts while the tab is open and needs no cookie, so no cookie banner. That catches people who read and buy in one sitting, which for low-priced indie tools is most of them.
If your buyers usually come back days later (higher prices, team purchases), add data-attribution="cookie" to the script tag. The id is then kept for a year in a first-party cookie on your own domain, so the later purchase links back to the visit that started it.
A cookie that tracks returning visitors may need consent where you operate (the EU’s ePrivacy rules, for example), so check before you turn it on.
<script defer src="https://bugotter.com/bo.js" data-app="YOUR_APP_ID" data-attribution="cookie"></script>3. Provider by provider
Each payment provider stores checkout data a little differently, so what gets linked differs. As of Nov 4, 2026:
| Provider | What you do | What gets linked |
|---|---|---|
| Stripe Payment Links | Nothing. 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 Subscriptions | Set 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. |
| Polar | Set metadata.bugotter_visitor_id on the checkout. | Orders, subscriptions and their renewals. |
| Creem | Set metadata.bugotter_visitor_id on the checkout (POST /v1/checkouts). | Subscriptions and their renewals. One-time payments aren’t linked yet. |
| Lemon Squeezy | Nothing 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_campaignvalues.
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.

- Channels, Sources, Campaigns. Three ways to slice where visits came from.
- Revenue per row. Social brought $175, Referral $29, next to visitors and error rate.
- Error insight. Referral visitors hit an error 6× more often than average, with a link to the error.
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
redditand 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.

- MRR across your connected providers, with the change since the start of the range.
- How MRR moved: new, upgrades, downgrades and cancellations as a waterfall.
- Forecast if the last 30 days repeat. The By source slice sits further down the page.
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.
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-ignoreat 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.