Every short link is a redirect handler, and a redirect handler sits in an unusually good spot: it sees a full HTTP request that the destination page will never show you. It is also the only place where a few well-known measurement bugs can be fixed.
I build HopQ, a link shortener with analytics, so I have spent more time in this request than is reasonable. Here is what is actually in it, and the four things that surprise people.
The request itself
A click arrives as an ordinary GET:
GET /a1b2c3 HTTP/1.1
Host: go.example.com
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) ...
Accept-Language: en-IN,en;q=0.9
Referer: https://www.instagram.com/
X-Forwarded-For: 49.36.x.x
From that you get device and OS (User-Agent), rough language and locale (Accept-Language), country and city (IP lookup), and where the click came from (Referer). Timestamp is free. That is the whole raw material.
1. Referer is missing exactly when you need it most
This is the reason "why is all my traffic direct?" is the most common analytics question in existence.
A referrer is only sent when a document navigates to another document. When someone taps a link inside the WhatsApp or Instagram app, the in-app webview opens the URL without a referring document, so there is nothing to send. The click is real, the human is real, and the header is empty.
Referrer-Policy removes the rest. strict-origin-when-cross-origin has been the browser default for years, so even a genuine web referrer arrives as https://instagram.com/ with the path stripped - you learn the site, never the post.
The fix is not technical, it is procedural: if you need to know which post drove the click, the link has to be different per post. No header will tell you afterwards.
2. A 301 will quietly end your analytics
Pick your redirect status carefully:
- 301 / 308 - permanent. Browsers are allowed to cache these, and they do, often indefinitely. The second click never reaches your server. Your click count flatlines and you can no longer change the destination for anyone who clicked once.
- 302 / 307 - found / temporary. Revalidated each time. Every click hits your handler.
For a shortener, 301 is almost always wrong. It is also close to unrecoverable: you cannot reach into a browser cache and undo it. Serve 302, and send Cache-Control: no-store with it.
w.Header().Set("Cache-Control", "no-store")
http.Redirect(w, r, dest, http.StatusFound) // 302, not 301
Search-engine advice says the opposite - 301 to consolidate ranking signals - which is correct for a moved page and wrong for a tracked link. Different job.
3. Most of your first "clicks" are link preview bots
Paste a URL into WhatsApp, Slack, Telegram, Discord, iMessage or Twitter and the platform fetches it immediately to build the preview card. That fetch hits your redirect handler and looks like a click.
They are recognisable, and the giveaway is not only the User-Agent:
- UA strings containing
WhatsApp,Slackbot-LinkExpanding,TelegramBot,Discordbot,facebookexternalhit,Twitterbot,bot,crawler,preview - no
Accept-Languageheader, which almost every real browser sends - arrival within a second or two of the link being created or pasted
- several requests from datacentre IP ranges in the same instant
If you do not filter these, a link shared into three group chats reports six clicks before a human has seen it. Everything downstream - CTR, "best performing channel" - is then wrong in a direction you cannot spot.
4. You can have geo without storing the IP
The IP is only needed for two things: deriving a location, and deciding whether two clicks are the same person. Both survive discarding it.
Resolve the country and city at request time, keep those, drop the address. For unique counts, store a keyed hash instead:
visitor_id = HMAC-SHA256(key = daily_rotating_salt, message = ip + user_agent)
Rotate the salt daily. You can count unique visitors within a day, the value is useless for identifying anyone, and there is no raw IP in your database to leak or to hand over. Under GDPR an IP address is personal data; a salted rotating hash is a much easier thing to defend.
While we are here: "dynamic" QR codes
A QR code is a picture of a string. That is the entire technology. So:
- Encode
https://mysite.com/menu-june.pdfand the code is static - the destination is physically printed into the pattern. Change the file and you reprint every table tent. - Encode
https://go.mysite.com/menuand the code is dynamic - not because the image is clever, but because the URL is a redirect you control. Repoint it whenever you like, and every printed copy follows.
There is no such thing as a dynamic QR code, only a QR code pointing at a mutable URL. Which also means a dynamic code can be counted, and a static one can never be.
The same indirection buys routing. Since the redirect sees the User-Agent, one code can send iOS to the App Store, Android to Play, and desktop to a landing page - or serve a breakfast menu before 11am and a dinner menu after 7pm, from the same printed square.
The bit worth remembering
The redirect is the only moment you control between the click and the page. Whatever you fail to record or decide there is gone: the destination page cannot recover the referrer, cannot un-cache a 301, and cannot tell the preview bots from the customers.
I put all of the above into HopQ - 302s, bot filtering, hashed visitor IDs, device and time routing, and a QR code for every link. If you are building your own, the four items above are the ones I would get right first.
Top comments (0)