What a share link sees
Visit analytics for my share links mostly measured me and some bots. The useful number took three filters to find.
The first thing visit analytics taught me is that most visits are not people. Last week I added per-visit tracking to my share links, the file-drop URLs I send to clients, and the table immediately filled with three species: me checking my own link, my own curl calls from testing, and the preview bots that fetch any URL the moment it lands in a chat app.
The build itself is deliberately plain. Views and downloads are separate event kinds, because "they opened it" and "they kept it" answer different questions about a proposal. Every event writes one row: IP address, country, the user agent parsed down to browser, OS, and device class plus a bot flag, the referrer, and the language header. There is also a meta column, a JSON catch-all, where the extras land without schema ceremony: the ISP, the organization behind the address, coordinates when the lookup offers them. Behind Cloudflare the real client address arrives in the CF-Connecting-IP header, so the row records a person and not a proxy. The write runs inside Next's after() hook, meaning the response has already gone out the door before any analytics work begins. Tracking adds zero milliseconds to the page a client sees, which is the correct price for a feature that exists for me and not for them.
Cloudflare only forwards the country by default. City and region come from resolving the IP against ipwho.is, a free, keyless lookup, called best-effort with a 2.5-second timeout, skipping private addresses entirely. For a dashboard with one user I will take keyless and best-effort over an API account and a quota every time. The failure mode is an empty city cell, not a broken page, and an empty cell is a fine price for never managing a key.
Then came the humbling part: filtering myself back out of my own numbers. Signed-in owner views were excluded from day one, and it was not enough, because I kept opening my own links signed out, on my phone, through curl. So the visits panel now prints my current IP at the top, badges matching rows with a small You, and a day after shipping it grew a bot filter, a hide-me filter, and a clear button, because the headline view count was still mostly noise wearing my face. The number that survived all the filtering is distinct IP addresses, minus mine. Everything else is decoration around it.
Why build it at all: for the same reason DocSend exists. When I send a proposal, knowing it was opened on Tuesday changes what I do on Wednesday. An IP address is personal data, and these are my own outbound documents going to people who asked for them; if this ever points at EU clients seriously, the share page grows a disclosure line to match.
So far the table says less about my visitors than about what the internet does to a URL the second it exists. Paste a link in a chat and machines you never invited have read it before the recipient does. I knew that abstractly. Now it has rows.