Gökhan Arkan

Building UTM Manager: A Lightweight Solution for Marketing Attribution

UTM parameters are everywhere in marketing. Every time someone clicks a link in your newsletter, a Google ad, or a social media post, those little query strings like ?utm_source=google&utm_medium=cpc tell your analytics platform where the visitor came from. It’s how marketing teams measure what’s working and what’s wasting budget.

The problem is, UTM parameters disappear the moment a user navigates to another page on your site. That click from your paid campaign? Gone. The attribution? Lost. And if you’re running a multi-page signup flow or an e-commerce checkout, you’ve just lost the thread connecting that conversion back to the campaign that drove it.

I ran into this at work. We had a custom tracking setup where UTM values needed to persist across sessions and pages, with configurable rules for how to handle returning visitors. When I looked for an open-source solution, I found either heavyweight analytics libraries that did far more than I needed, or abandoned packages that hadn’t been updated in years. Nothing lightweight, framework-agnostic, and actively maintained.

So I built UTM Manager.

The Problem in Detail

If you’re not familiar with UTMs, here’s the quick version: UTM (Urchin Tracking Module) parameters are standardised query strings that marketing teams append to URLs to track campaign performance. There are five of them:

ParameterPurpose
utm_sourceTraffic source (google, newsletter, twitter)
utm_mediumMarketing medium (cpc, email, social)
utm_campaignCampaign identifier (spring_sale, product_launch)
utm_termPaid search keywords
utm_contentDifferentiate similar content or links

When a user lands on yoursite.com?utm_source=google&utm_medium=cpc, Google Analytics picks up those parameters and records them. Simple enough, until the user clicks to another page. Now they’re on yoursite.com/pricing, the URL is clean, and the UTM context is gone.

For single-page apps this is less of an issue since the URL doesn’t change between views. But most real-world sites have multi-page flows: landing pages, product pages, checkout processes, signup wizards. Every page transition is an opportunity to lose attribution data.

There are three core challenges:

  1. Persistence. UTM parameters need to survive page navigation. The standard approach is cookies, but cookie handling is fiddly; expiration, domain scope, security flags, and the differences between how browsers handle them.

  2. Attribution strategy. When a user returns to your site with new UTM parameters, what do you do? If they came from a Google ad last week and an email this week, which source gets credit? Marketing teams have strong opinions about this. Some want “first-touch” attribution (credit the original source), others want “last-touch” (credit the most recent). Some have complex rules based on campaign type or traffic value.

  3. Framework compatibility. Your marketing site might be vanilla HTML. Your app might be React. Your e-commerce platform might be Next.js with server-side rendering. Your WordPress blog needs UTM tracking too. Any solution needs to work across all of these without forcing you to rewrite your stack.

Designing the Solution

The requirements were clear from the work problem:

The architecture ended up with four layers:

┌─────────────────────────────────────┐
│  Framework Adapters                 │
│  (React hooks, Next.js, WordPress)  │
├─────────────────────────────────────┤
│  Business Logic                     │
│  (Attribution, validation, config)  │
├─────────────────────────────────────┤
│  Data Extraction                    │
│  (Parse UTMs from URLs/queries)     │
├─────────────────────────────────────┤
│  Storage Layer                      │
│  (Cookie management)                │
└─────────────────────────────────────┘

Each layer has a single responsibility. The storage layer knows nothing about UTMs. It just handles cookies securely. The extraction layer parses URLs. The business logic layer applies attribution rules and validation. The framework adapters translate between the core API and framework-specific patterns (React hooks, Next.js router integration, etc.).

Attribution: The Feature That Actually Matters

Here’s where it gets interesting for marketing teams. Attribution strategy determines which traffic source gets credit when users visit multiple times with different UTMs.

First-touch attribution keeps the original UTM values forever (or until cookies expire). A user clicks your Google ad in January, comes back organically in March, and converts in April. Google gets the credit because that’s what introduced them to you.

UTMManager.configure({
  attribution: 'first',
  expirationDays: 90,
});

This is common in B2B with long sales cycles. You want to know what started the conversation, not what happened to be the last touchpoint before conversion.

Last-touch attribution always overwrites with the most recent values. Same scenario: the April conversion gets attributed to organic because that’s what drove the final visit.

UTMManager.configure({
  attribution: 'last', // This is the default
});

E-commerce often prefers this. Shorter purchase cycles mean the most recent touchpoint is usually most relevant.

Dynamic attribution is where it gets powerful. Some businesses have rules like “paid traffic always takes priority over organic” or “email campaigns should only be credited if they’re within 7 days of the visit.” These don’t fit neatly into first/last buckets.

UTMManager.configure({
  attribution: 'dynamic',
  attributionCallback: (current, incoming) => {
    // Paid traffic always wins
    if (incoming.includes('cpc') || incoming.includes('paid')) {
      return incoming;
    }
    // Otherwise keep what we have
    return current;
  },
});

The callback receives the current stored value and the incoming value from the URL, returning whichever should be saved. You can implement whatever logic your marketing team dreams up.

Framework Integrations

I built UTM Manager to work everywhere without being tightly coupled to any framework. The core is ~2KB gzipped and has zero dependencies. Framework integrations are separate entry points that you only import if you need them.

Vanilla JavaScript

Drop in a script tag and you’re done:

<script src="https://unpkg.com/utm-manager/dist/utm-manager.min.js"></script>

The script automatically captures UTM parameters from the URL on page load:

// Get all stored parameters
const params = UTMManager.getAllUTMs();
// { utm_source: 'google', utm_medium: 'cpc', ... }

// Get a specific one
const source = UTMManager.getUTM('utm_source');

// Listen for updates
window.addEventListener('utmParametersUpdated', (e) => {
  console.log('UTMs updated:', e.detail);
});

React

A hooks-based API that feels native to React:

import { useUTMs } from 'utm-manager/react';

function SignupForm() {
  const { params, setParam, captureFromURL } = useUTMs({
    autoCapture: true,
    attribution: 'first',
    onUpdate: (params) => {
      // Send to analytics, attach to form, etc.
      analytics.identify({ ...params });
    },
  });

  return (
    <form>
      {/* Hidden fields for form submission */}
      {Object.entries(params).map(([key, value]) => (
        <input type="hidden" name={key} value={value} key={key} />
      ))}
      {/* Rest of your form */}
    </form>
  );
}

The hook manages state, handles cleanup, and provides methods for manual capture if you need them.

Next.js

Next.js has quirks. The router exposes query parameters differently during server-side rendering versus client-side navigation. The useNextUTMs hook handles this:

import { useNextUTMs } from 'utm-manager/next';

function LandingPage() {
  const { params } = useNextUTMs({ autoCapture: true });

  return (
    <div>
      {params.utm_campaign && (
        <p>Welcome to our {params.utm_campaign} promotion!</p>
      )}
    </div>
  );
}

Internally, it integrates with Next.js router events and handles the router.query format (which can return arrays for repeated parameters) correctly.

WordPress

WordPress sites often use jQuery, so the WordPress adapter fires jQuery events alongside standard DOM events:

jQuery(document).on('utm_manager_ready', function(e, utms) {
  console.log('UTMs:', utms);
});

Same core, different interface for compatibility.

Common Use Cases

Attaching UTMs to Form Submissions

The most common pattern is passing UTM data to your CRM or marketing automation platform when leads submit forms:

document.querySelector('form').addEventListener('submit', function() {
  const utms = UTMManager.getAllUTMs();

  Object.entries(utms).forEach(([key, value]) => {
    const input = document.createElement('input');
    input.type = 'hidden';
    input.name = key;
    input.value = value;
    this.appendChild(input);
  });
});

Now your CRM knows which campaign drove each lead.

Analytics Integration

Fire events when UTM data changes, useful for custom analytics or A/B testing platforms:

window.addEventListener('utmParametersUpdated', (e) => {
  analytics.track('Campaign Visit', e.detail);

  // Or update a data layer for Google Tag Manager
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    event: 'utm_captured',
    ...e.detail,
  });
});

Cross-Domain Tracking

If you have multiple domains (marketing site on www.example.com, app on app.example.com), configure a shared cookie domain:

UTMManager.configure({
  domain: '.example.com', // Note the leading dot
  expirationDays: 30,
});

Cookies will be accessible across subdomains.

Technical Decisions

A few implementation details that might interest developers:

Validation. UTM Manager only accepts the five standard UTM parameters. Try to save utm_custom and you’ll get an error. This prevents typos and ensures compatibility with analytics platforms that expect the standard parameters.

Cookie security. All cookies are set with Secure and SameSite=Lax by default. This follows modern best practices and prevents CSRF-adjacent issues.

Multi-format distribution. The build outputs four formats: ESM and CommonJS for npm users (supporting both import and require), plus an IIFE bundle for direct browser usage via CDN. React and Next.js integrations are separate entry points so vanilla JS users don’t bundle React.

Event-driven updates. The utmParametersUpdated event fires whenever UTM data changes. This decouples storage from reaction, your code doesn’t need to poll for changes or import state management libraries.

Getting Started

Install via npm:

npm install utm-manager

Or use the CDN for vanilla JavaScript:

<script src="https://unpkg.com/utm-manager/dist/utm-manager.min.js"></script>

Basic usage:

// Auto-captures on load, then:
const allParams = UTMManager.getAllUTMs();
const source = UTMManager.getUTM('utm_source');

// Configure as needed
UTMManager.configure({
  attribution: 'first',
  expirationDays: 90,
});

The live demo lets you test different configurations and see the cookie storage in action.

The Broader Point

Marketing attribution is unglamorous work, but it’s foundational. When it’s broken, or not implemented, marketing teams fly blind. They can’t tell which campaigns drive real value versus which ones just generate traffic. Budget gets allocated based on gut feel rather than data.

The gap I found wasn’t technical complexity. It was that existing solutions were either over-engineered (full analytics platforms when all you need is parameter persistence) or under-maintained (abandoned npm packages with security vulnerabilities).

UTM Manager does one thing well: capture, persist, and retrieve UTM parameters with sensible defaults and flexible configuration. It’s ~2KB, has zero dependencies, works across frameworks, and is MIT licensed.

If you’re building something that needs UTM tracking, give it a try. If you find edge cases or have feature requests, the code is on GitHub.


URLs in this post: