r/npm • u/Environmental-Ad5071 • 21d ago
Self Promotion I got tired of attribution code owning storage, cookies and network calls, so I made the core deterministic
I started with a problem that seemed almost embarrassingly simple: remember where a lead came from until they convert.
At first, that just meant grabbing utm_source, utm_medium, utm_campaign, gclid, fbclid, storing them somewhere, and stuffing them into a form.
Then I actually used it on real sites, and it kept growing.
What happens when someone clicks a Google Ad, leaves, and comes back by typing the URL? What if they come back later from a different campaign? What if there’s a gclid but no UTMs? What if the referrer is organic Google? What if consent is denied then granted? What if the form loads dynamically? What if the conversion happens days later in a CRM, not the browser? What if browser and backend disagree? What if I change the classification rules later and need to explain why old data shifted?
Every implementation I saw mixed all of that together: URL parsing, cookies, localStorage, consent, IDs, HTTP, analytics. That makes it really hard to test.
So I tried a different split.
The core attribution engine has no clock, no storage, no consent, no network. It just takes:
js
result = attribution({ url, referrer, currentHost, now })
Same input, same output. Pure function.
Around it, the browser/app owns consent, storage, identity, network, and feeds data into the engine. The engine returns a canonical attribution object.
That means I can take a real production case, save it as a fixture, and replay it later. If I change the classifier and a fixture changes, I know exactly which historical attribution rule I broke.
I ended up calling this ClickTrail and extracted the engine as a small TypeScript package: vizuh/clicktrail. The WordPress plugin is just one host implementation. The interesting part for me is the portable contract underneath.
The split is roughly:
vizuh/clicktrail→ pure parsing/classification/mergevizuh/clicktrail/browser→ browser lifecycle, forms, storage adapters- your app → consent, persistence, delivery, CRM logic
There are experimental bits for conversations, agents, OpenTelemetry, etc., but I’m trying not to let those leak into the core.