MailSignals

Link tracking

A click is the strongest signal short of a reply. Attribute it without risking your domain.

A click survives image blocking and means someone did something. But routing a recipient's traffic through your own host puts two things at risk: the tracking domain's reputation, and your deliverability. Link tracking in MailSignals is designed around both.

To: noor@partner.example
Onboarding checklist for the pilot

Everything you need is at docs.mycompany.example/pilot and the calendar link is calendar.google.com/…

The unsubscribe link in the footer stays exactly as it is.

Send✓✓ Tracking on
Both typed URLs become links; the first is signed through your tracking host, the calendar link is left alone because redirecting it would strip context Google needs, and the unsubscribe link is never touched.

What a tracked link looks like

https://t.yourdomain/l/<token>.<idx>?u=<destination>&s=<sig>

The signature is HMAC-SHA256 over the token, the link's index and its destination, with a per-user key, truncated to 128 bits. The redirect verifies before it does anything else; a failed signature is a 400 and never a redirect. Without that, the endpoint would be an open redirector and the domain blacklisted within days.

The destination travels in the URL rather than being looked up, so a click can be served fast and can arrive even before the link table exists. The key never leaves the service worker: the content script asks for finished URLs, so a compromised page script cannot mint its own.

Why the URL avoids certain words

No "track" or "click" in the path: those are exactly what tracker blocklists match on. /l/ is unremarkable. The tracking host serves nothing else, so its reputation is never shared with a dashboard or a marketing site, and never with another user.

Finding the links

Link work runs twice: a pass over the live editor at the moment you press Send, so the first registration already carries your intent, and an authoritative pass over the exact HTML Gmail is about to send.

Rewritten

  • Anchors in the new content of the message, in document order.
  • URLs typed as text: anything with an http(s) scheme, anything starting www., and a bare domain whose last label is one of 73 common TLDs. Trailing punctuation stays text.
  • Links whose visible text is a bare URL, by default.

Never rewritten

  • Anything inside quoted text. Rewriting a link in a quote would silently re-point someone else's earlier message.
  • Anything inside your Gmail signature.
  • Unsubscribe and opt-out links. A broken one is both a bad experience and, for bulk mail, a legal problem.
  • Calendar, Meet, Zoom, Teams and mailing-list management hosts, where a redirect breaks the destination or strips context the service needs. Matched on the parsed hostname, never on a substring of the URL.
  • In-page anchors, non-http(s) schemes, and links already pointing at the tracking host.

Version numbers, file names and abbreviations are deliberately not linkified; the tests assert v1.0, report.pdf, Node 22.3, e.g. this and U.S. economy stay text. The detail page reports how many of your own links were left as written, and why.

Telling a click from a scan

A corporate gateway follows every link before delivery. Three defences, and a column that keeps the evidence.

  1. Too fast. A click under 3 seconds after sending is a scanner.
  2. A burst. Three or more distinct links within 2 seconds means something walked every link in the message. Context is the ten most recent clicks on the message.
  3. Unrecognised does not count. An unrecognised client is what a script looks like, and any real browser lands in "Opened".

Bot clicks are kept, not dropped: knowing a gateway walked your links is itself information, and the totals stay reconcilable against the event log. The Links page shows human and bot clicks in separate columns and dims rows with only bot traffic. First and last click times move only for human clicks.

What happens on a click

The redirect endpoint step by step
StepBehaviour
Shape checkMalformed token or parameters: 400
Negative cacheA token that recently missed: 404 without a database read
VerifyThe key is reached through the message, so a click on a message whose registration never landed is refused rather than redirected blindly
RedirectThe 302 is sent before recording; the visitor is never delayed by our bookkeeping
RecordClassified with burst context; event, message counters and the link document updated; the link document is created on demand if registration never sent it

Where it lives

shared/src/links.ts
Eligibility policy and typed-URL detection.
shared/src/tokens.ts
URL building, signing, verification.
extension/src/gmail/compose.ts
Both passes, marking, and swapping signed URLs into the outgoing HTML.
functions/src/track.ts
The redirect endpoint.
scripts/e2e-links-replies.mjs
Per-link attribution, query strings surviving the round trip, gateway and burst filtering, against a deployment.

Related: the click rules · the Links page.

Run it in your own project.

One Firebase project, two subdomains, one deploy script. About an hour the first time, and no subscription afterwards.