CTracker runs a write-read test when it starts. If storage is blocked, private browsing on older Safari for example, the class steps aside and the backend middleware still reads the parameters straight from the request. The attribution chain is shorter in that case, but the visit is never lost.
Services
Full-Stack Development, Data Engineering
Industry
B2B Marketplace
Year
2024
Campaign attribution without a third-party script.
30 to 40 percent of a marketing dashboard was missing, and nobody could see the hole. A B2B marketplace paid for ads on Google, LinkedIn and industry portals and judged them in Google Analytics. Ad blockers erased a third of all visits before the script loaded. Six-figure budget decisions rested on the visits that got through.
THE CHALLENGE
A report with no memory
The blocked visits were only the visible loss. The GDPR consent banner cut coverage further, and what did arrive lived in a dashboard that could not be joined to the CRM: a signup was a row in one system and a session in another. The tools also could not tell the first touchpoint from the most recent one. A buyer who clicked a LinkedIn ad, browsed for days and signed up weeks later through an industry portal showed up as a portal conversion. The campaign that started the relationship was gone. Marketing was comparing channels with data that forgot where people came from.
THE SOLUTION
Attribution inside the application boundary
The first proposal was a second tool, Segment, behind a proper consent flow. Rejected: the same browser extensions block it, and the banner costs more coverage than it protects. Instead the platform tracks itself. A lightweight JavaScript class, CTracker, reads c_source, c_medium and c_campaign from the URL on the first visit and keeps them in localStorage. No cookies, no external requests. On the server, two Django middlewares, CTrackMiddleware and LeadSourceMiddleware, read the same parameters from the query string and the session, validate them against known sources and write LeadSource records to PostgreSQL. When the visitor signs up or logs in, a Django signal binds every stored record to the new account. The trade-off is ownership: there is no vendor dashboard and no cross-site view, and the platform team maintains the code. In return, every visit is counted, including the ones the blockers would have erased.
The frontend class. The guard on the initial slot is the whole trick:
JavaScript
class CTracker {
constructor () {
this.storageAvailable = this._testStorage();
if (!this.storageAvailable) return;
const params = new URLSearchParams(window.location.search);
const keys = ['c_source', 'c_medium', 'c_campaign', 'c_term'];
keys.forEach(key => {
const val = params.get(key);
if (!val) return;
// Always update the "current" touchpoint
localStorage.setItem(key, val);
// Set "initial" once, never overwrite it
if (!localStorage.getItem(key + '_initial')) {
localStorage.setItem(key + '_initial', val);
}
});
}
_testStorage () {
try {
localStorage.setItem('__ct', '1');
localStorage.removeItem('__ct');
return true;
} catch { return false; }
}
}One visitor, 26 days
The journey plays on its own. Watch the two slots: initial is written on day 1 and never changes, current follows every tagged visit. Switch the ad blocker off to see what a third-party script would have recorded, then back on mid-journey: everything after the switch is lost to it, and nothing is lost to the first-party row.
Visitor journey
- Day 1LinkedIn adc_source=linkedin
- Day 3Organic returnno c_ parameters
- Day 12Google adc_source=google
- Day 20Industry portalc_source=portal
- Day 26Signupaccount created
First-party pipeline
- initial
- linkedin · paid
- current
- portal · referral
- Day 1 · recorded
- Day 3 · recorded
- Day 12 · recorded
- Day 20 · recorded
Bound to the new account · Touchpoints in the chain: 4
Third-party script
- last touch
- no data
- Day 1 · blocked
- Day 3 · blocked
- Day 12 · blocked
- Day 20 · blocked
no data
- Touchpoints seen
- 4
- Attributed first-party
- 4
- Recorded third-party
- 0
- Lost to the blocker
- 4
On the server, the middleware writes one LeadSource per session and attaches the user as soon as there is one:
Python
class LeadSourceMiddleware:
"""
Reads c_* attribution params from query string + session,
validates them, and persists a LeadSource record.
"""
def __call__(self, request):
params = self._extract(request)
if not params:
return self.get_response(request)
source, created = LeadSource.objects.get_or_create(
session_key=request.session.session_key,
defaults=params,
)
# Bind to user when they authenticate
if request.user.is_authenticated and not source.user:
source.user = request.user
source.save(update_fields=['user'])
return self.get_response(request)THE RESULT
Every signup carries its history
Every CRM record now shows where a customer first came from, where they came from last and every tagged visit in between, bound to the account at signup. Ad blockers stopped mattering because nothing external loads. The GDPR position became simple: no third-party cookies, no cross-site tracking, no consent banner for first-party analytics. The marketing team compares channels on complete data and does it inside the CRM it already uses, for thousands of visitors a day.
KEY METRICS
40%Share of visits recovered from ad blockers
100%Visits attributed, first-party
250msAdded latency per request, at most
FOR YOUR PROJECT
- When it applies
You buy traffic and judge it in a third-party dashboard. Ad blockers and consent banners are already hiding part of your funnel, and a session in the dashboard cannot be joined to a signup in your CRM.
- What to check
Open your signup table. If no source columns are filled at account creation, attribution ends at the session and starts over on every return visit. The first campaign is already lost.
- What it needs
A small frontend class, a middleware pair and one table in the database you already run. No new vendor, no consent banner for first-party data, and your own team maintains it.
CLIENT FEEDBACK
We open a signup in the CRM and see the ad that brought the person in and the visit that converted. Budget meetings run on that list now, not on a dashboard that was missing a third of the visits.
Head of Marketing
B2B marketplace
FAQ
TECHNOLOGY STACK
Django
Python
JavaScript
Manuel Kasbarian - CEO, SophistiXWe have enjoyed working with Daniel for 10 years now. We highly appreciate his fast response times around the clock and his all-round knowledge. Whether server configurations or programming, he always has the right solution.
Join Manuel and launch your next project with confidence.
Open to new ProjectsGet In Touch