@latimer-woods-tech/attribution
v0.1.0
Published
The attribution spine: Worker-safe, zero-dependency UTM + referrer touch capture, first/last-touch journey helpers, a documented last-non-direct conversion model, compact cookie/KV serialization, a storage-agnostic touch store port, and a bot/human (synth
Readme
@latimer-woods-tech/attribution
The attribution spine. Capture where a visitor came from, keep an ordered touch journey, credit the channel that produced a buyer, and separate real humans from bots and our own probes — so a funnel means something.
Zero runtime dependencies, no Node built-ins — safe inside a Cloudflare Worker.
The problem
The portfolio could not answer the single most important marketing question — "which channel produced this buyer?" There was zero UTM capture anywhere, and the funnels were inflated by ~29 autonomous loops and synthetic monitors hitting the same public endpoints as humans. You cannot spend to acquire when you cannot measure acquisition, and you cannot trust a conversion rate that counts your own robots.
Capture a touch
import { parseAttribution } from '@latimer-woods-tech/attribution';
const touch = parseAttribution(
'https://selfprime.net/quiz?utm_source=beehiiv&utm_medium=email&utm_campaign=lunation-07',
request.headers.get('referer'),
{ selfHosts: ['selfprime.net'] },
);
// → { source: 'beehiiv', medium: 'email', campaign: 'lunation-07', term: null,
// content: null, referrerHost: '…', landingPath: '/quiz', ts: 1_700_000_000_000 }Rules, in priority order:
- UTM parameters win. Any of
utm_source/utm_medium/utm_campaignmarks an explicitly tagged campaign; the tags are the source of truth. - Otherwise the referrer is classified —
organic(a search engine),social(a known network), orreferral(any other external host). - No usable referrer →
direct. A referrer matchingselfHostsis internal navigation, alsodirect— never a bogus self-referral.
Deterministic: inject now to pin the timestamp.
Journey: first touch, last touch, conversion
import { mergeTouch, attributeConversion } from '@latimer-woods-tech/attribution';
let touches = [];
touches = mergeTouch(touches, firstVisit); // ordered oldest-first, capped
touches = mergeTouch(touches, returnVisit);
attributeConversion(touches); // last-non-direct-click (default)
attributeConversion(touches, 'first-touch'); // or first / last touchmergeTouch maintains an ordered, capped list. When the cap is exceeded the very
first touch is always preserved (first-touch attribution must survive trimming),
alongside the most-recent window; consecutive same-channel hits collapse into a
recency refresh rather than a duplicate hop.
attributeConversion credits one source. The default last-non-direct-click model
walks back past a direct return visit to the last channel that actually referred the
visitor — a bookmark reopened right before purchase should not steal credit from the
newsletter that drove them.
Store the journey
import { InMemoryAttributionStore, recordTouch } from '@latimer-woods-tech/attribution';
const store = new InMemoryAttributionStore(); // reference impl for tests
await recordTouch(store, anonId, parseAttribution(url, ref));AttributionStore is a narrow get/put port (mirrors
@latimer-woods-tech/operator): a KV-, cookie-, or Neon-backed adapter is a drop-in
replacement. Or persist the list yourself in a cookie:
import { serializeTouches, deserializeTouches } from '@latimer-woods-tech/attribution';
const cookie = serializeTouches(touches); // compact base64url, no Buffer
const touches = deserializeTouches(cookie ?? ''); // total — bad input yields []Bot / human separation
import { classifyTraffic } from '@latimer-woods-tech/attribution';
const { isSynthetic, reason } = classifyTraffic({
userAgent: request.headers.get('user-agent'),
// ONLY true after you HMAC-verify the X-Synthetic-Probe header upstream:
syntheticProbeHeaderPresent: verifiedProbe,
});Combines user-agent heuristics (known bots, headless markers, HTTP libraries, uptime
monitors) with the X-Synthetic-Probe convention (the-calling#45 / HD#1005). This
primitive consumes an already-verified probe flag — it never inspects a raw header
or runs the HMAC (that stays with the caller that holds the secret). A forged or
absent signal counts as real, so a bot cannot label itself synthetic to slip out
of the funnel.
De-pollution can make your numbers look worse, and that is correct. When you stop counting your own probes and crawlers, completion rates fall to their true human value. Never act on pre-launch funnel metrics; the decision baseline is the first real human cohort.
What this does not do
This package is pure measurement primitives. It does not mint anon ids, set cookies, run the synthetic-probe HMAC, talk to a database, or decide spend. Those are the caller's job — this is the spine they hang on.
License
MIT © Latimer Woods Tech
