Back to Homepage
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
  1. Day 1LinkedIn adc_source=linkedin
  2. Day 3Organic returnno c_ parameters
  3. Day 12Google adc_source=google
  4. Day 20Industry portalc_source=portal

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

We 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.

Manuel Kasbarian - CEO, SophistiX

Join Manuel and launch your next project with confidence.

Get In TouchGet In Touch