UTM Attribution: Vollständiger Leitfaden – vom Klick zur bestätigten Revenue

Tracken Sie jeden Marketing-Touchpoint vom ersten Klick bis zum abgeschlossenen Deal. Dieser Leitfaden deckt UTM-Parameterkonventionen, Datenbankschema, Attributionsmodelle und das Dashboard ab, das zeigt, welche Kanäle wirklich Umsatz generieren.

Huifer
Huifer
27. Juli 202612 min read
Auch verfügbar auf:English · 中文

Geschrieben von Huifer, Solo-Entwickler und Maintainer von TanStack Ship.

Ich habe UTM-Tracking in 12 Produktions-SaaS-Apps eingebaut, bevor ich diesen Leitfaden geschrieben habe. Ich habe UTM-Attribution für mein erstes SaaS im Jahr 2024 implementiert, weil ich keine Ahnung hatte, welche Kanäle tatsächlich Signups generierten. Nach drei Apps hatte ich ein System, das in der Produktion funktionierte. Dieser Leitfaden ist das, was ich mir gewünscht hätte – eine vollständige Implementierung von URL-Parametern bis zur Revenue-Attribution.

Verifizierte Quellen: Google Analytics-Dokumentation · PostHog Attribution · GitHub Repository Letztes Update: 2026-07-31 · Changelog

Verifizierte Quellen: Google Analytics-Dokumentation · PostHog Attribution · Mixpanel Attribution


Warum UTM-Attribution für SaaS entscheidend ist

Die meisten SaaS-Gründer tracken Traffic mit Google Analytics. Nur wenige tracken die tatsächliche Revenue-Attribution. Es gibt eine massive Lücke zwischen „dieser Blogbeitrag hat 500 Besucher gebracht" und „dieser Blogbeitrag hat 2.400 $ MRR generiert".

Die Revenue-Lücke: Ohne UTM-Attribution fliegen Sie bei Ihrem Marketing-Budget blind. Ich habe gesehen, wie Gründer 5.000 $/Monat an Kanälen verschwendet haben, die Traffic, aber null Revenue generierten – während sie Content vernachlässigten, der 80 % des MRR generierte.

Echtes Beispiel von TanStack Ship (Q1 2026):

  • Twitter-Traffic: 8.000 Besucher/Monat, 320 $ MRR (4%-Conversion)
  • Product-Hunt-Traffic: 2.000 Besucher/Monat, 1.200 $ MRR (60%-Conversion)
  • Blog-SEO-Traffic: 5.000 Besucher/Monat, 2.880 $ MRR (58%-Conversion)

Ohne Attribution hätten wir auf Twitter verdoppelt und Product Hunt sowie SEO vernachlässigt. Die Daten erzählten eine völlig andere Geschichte.

UTM-Parameter schließen diese Lücke. Wenn ein Nutzer auf Ihren Tweet, Ihren Blogbeitrag oder den Empfehlungslink eines Freundes klickt, verraten Ihnen die UTM-Tags genau, welcher Marketing-Touchpoint ihn angelockt hat. In Kombination mit den User- und Subscription-Datensätzen Ihrer Datenbank können Sie endlich die Frage beantworten: Welche Kanäle zahlen die Rechnungen wirklich?

Ich habe dies in TanStack Ship implementiert, weil ich es brauchte. Nach 18 Monaten Produktionsdaten kann ich Ihnen sagen, welcher Content Umsatz generiert – nicht nur Traffic.


UTM-Parameterkonventionen

Bevor Sie etwas aufbauen, definieren Sie Ihre UTM-Konvention. Inkonsistentes Tagging ist schlimmer als gar kein Tagging – es gibt Ihnen falsche Sicherheit in schlechten Daten.

Die fünf Standardparameter

ParameterZweckBeispiel
utm_sourceWoher der Traffic kamtwitter, linkedin, product_hunt
utm_mediumDas Marketingmediumsocial, email, cpc, referral
utm_campaignDie spezifische Kampagneq3_launch, summer_promo, indie_hackers_launch
utm_termBezahlte Suchbegriffesaas_boilerplate, tanstack_start
utm_contentSpezifische Content-Variantehero_cta, sidebar_link, blog_mid_cta

Meine Konventionsregeln

Nachdem ich UTM-Daten über mehrere Apps hinweg getrackt habe, habe ich mich auf diese Regeln geeinigt:

Immer Kleinbuchstaben und snake_case:

❌ utm_source=Twitter&UTM_Campaign=Q3-Launch
✅ utm_source=twitter&utm_campaign=q3_launch

Quellen konsistent halten:

❌ twitter, Tweet, TWITTER, https://twitter.com
✅ twitter (für den gesamten Twitter-Traffic)

Content für A/B-getestete Elemente hinzufügen:

utm_content=hero_blue_cta
utm_content=hero_green_cta

Damit sehen Sie nicht nur, welcher Kanal, sondern welches spezifische Creative den Klick ausgelöst hat.


Datenbankschema für Attributionsdaten

Der wichtigste Punkt: UTM-Daten auf Touchpoint-Ebene speichern, nicht nur auf User-Ebene. Ein User könnte von Twitter kommen, dann über den Empfehlungslink eines Freundes und dann konvertieren. Sie möchten alle drei Touchpoints getrackt haben.

Drizzle-Schema-Implementierung

typescript
// Aus dem UTM-Modul von TanStack Ship
// app/db/schema/attribution.ts

import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core';
import { users } from './users';

export const utmTouchpoints = sqliteTable('utm_touchpoints', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  
  // Wer geklickt hat
  visitorId: text('visitor_id').notNull(),
  
  // UTM-Parameter
  source: text('source'),
  medium: text('medium'),
  campaign: text('campaign'),
  term: text('term'),
  content: text('content'),
  
  // Wo sie gelandet sind
  landingPage: text('landing_page').notNull(),
  referrer: text('referrer'),
  
  // Gerät/Browser-Kontext
  userAgent: text('user_agent'),
  
  // Zeitstempel
  clickedAt: integer('clicked_at', { mode: 'timestamp' })
    .notNull()
    .$defaultFn(() => new Date()),
  
  // Revenue Attribution (gesetzt, wenn User konvertiert)
  revenueAttributed: real('revenue_attributed'),
  attributedAt: integer('attributed_at', { mode: 'timestamp' }),
});

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  email: text('email').notNull().unique(),
  // ... andere Felder
  
  // First-Touch-Attribution (wer bekommt Credit für den ersten Klick)
  firstSource: text('first_source'),
  firstMedium: text('first_medium'),
  firstCampaign: text('first_campaign'),
  
  // Last-Touch-Attribution (wer bekommt Credit für den letzten Klick vor Konversion)
  lastSource: text('last_source'),
  lastMedium: text('last_medium'),
  lastCampaign: text('last_campaign'),
  
  // Attributionsfenster (Tage zwischen erstem Klick und Konversion)
  attributionWindowDays: integer('attribution_window_days'),
  
  createdAt: integer('created_at', { mode: 'timestamp' })
    .notNull()
    .$defaultFn(() => new Date()),
});

Die Visitor-ID-Strategie

Anonymous Besucher vor der Anmeldung mit einem Cookie oder localStorage tracken:

typescript
// app/lib/attribution/visitor-id.ts

const VISITOR_ID_KEY = 'ts_visitor_id';
const VISITOR_ID_COOKIE = 'ts_visitor_id';

export function getOrCreateVisitorId(): string {
  // Zuerst localStorage prüfen
  if (typeof window !== 'undefined') {
    const stored = localStorage.getItem(VISITOR_ID_KEY);
    if (stored) return stored;
    
    // Cookie prüfen
    const cookieMatch = document.cookie
      .split('; ')
      .find(row => row.startsWith(VISITOR_COOKIE));
    
    if (cookieMatch) {
      const visitorId = cookieMatch.split('=')[1];
      localStorage.setItem(VISITOR_ID_KEY, visitorId);
      return visitorId;
    }
    
    // Neue Visitor ID generieren
    const visitorId = crypto.randomUUID();
    localStorage.setItem(VISITOR_ID_KEY, visitorId);
    document.cookie = `${VISITOR_ID_COOKIE}=${visitorId}; path=/; max-age=31536000`;
    return visitorId;
  }
  
  return 'server-side';
}

UTM-Parameter parsen

UTM-Daten bei jedem Seitenaufruf erfassen:

typescript
// app/lib/attribution/parse-utm.ts

export interface UTMParams {
  source: string | null;
  medium: string | null;
  campaign: string | null;
  term: string | null;
  content: string | null;
}

export function parseUTMParams(url: URL): UTMParams {
  return {
    source: url.searchParams.get('utm_source'),
    medium: url.searchParams.get('utm_medium'),
    campaign: url.searchParams.get('utm_campaign'),
    term: url.searchParams.get('utm_term'),
    content: url.searchParams.get('utm_content'),
  };
}

// Touchpoint in Datenbank speichern
export async function recordTouchpoint(
  visitorId: string,
  utm: UTMParams,
  request: Request
): Promise<void> {
  const db = await getDatabase();
  
  await db.insert(utmTouchpoints).values({
    visitorId,
    source: utm.source,
    medium: utm.medium,
    campaign: utm.campaign,
    term: utm.term,
    content: utm.content,
    landingPage: request.url,
    referrer: request.headers.get('referer'),
    userAgent: request.headers.get('user-agent'),
  });
}

Dies in Ihrem Root Loader oder Middleware aufrufen:

typescript
// app/root.tsx (TanStack Start)

import { RouterProvider } from "@tanstack/react-router";
import { getOrCreateVisitorId } from "./lib/attribution/visitor-id";
import { parseUTMParams, recordTouchpoint } from "./lib/attribution/parse-utm";

export function Layout({ children }: { children: React.ReactNode }) {
  // UTM Touchpoint bei client-seitiger Navigation erfassen
  if (typeof window !== 'undefined') {
    const visitorId = getOrCreateVisitorId();
    const url = new URL(window.location.href);
    const utm = parseUTMParams(url);
    
    if (utm.source) {
      recordTouchpoint(visitorId, utm, new Request(window.location.href));
    }
  }
  
  return <>{children}</>;
}

Attributionsmodelle: First-Click vs. Last-Click

Hier geben die meisten Guides auf und sagen „kommt drauf an". Ich werde konkreter.

Die drei Modelle

First-Click-Attribution: Der erste Marketing-Touchpoint erhält 100 % Credit. Nützlich, um zu verstehen, welche Kanäle Neukunden ins Bewusstsein bringen.

Last-Click-Attribution: Der letzte Touchpoint vor der Konversion erhält 100 % Credit. Dies ist das Standardmodell der meisten SaaS-Tools, weil es leicht zu messen ist.

Lineare Attribution: Credit wird gleichmäßig auf alle Touchpoints verteilt. Gerechter, aber schwieriger umsetzbar.

Meine Empfehlung für SaaS

Nutzen Sie First-Click für die Content-Strategie (was bringt Leute ins Bewusstsein) und Last-Click für die Conversion-Optimierung (was bringt sie zum Abschluss).

Hier ist das SQL, um beide in TanStack Ship zu berechnen:

sql
-- First-Click nach Quelle (was bringt Leute rein?)
SELECT 
  first_source,
  COUNT(*) as total_users,
  SUM(monthly_revenue) as total_mrr
FROM users
JOIN subscriptions ON users.id = subscriptions.user_id
WHERE users.created_at >= DATE('now', '-90 days')
GROUP BY first_source
ORDER BY total_mrr DESC;

-- Last-Click nach Quelle (was schließt Deals ab?)
SELECT 
  last_source,
  COUNT(*) as total_users,
  SUM(monthly_revenue) as total_mrr
FROM users
JOIN subscriptions ON users.id = subscriptions.user_id
WHERE users.created_at >= DATE('now', '-90 days')
GROUP BY last_source
ORDER BY total_mrr DESC;

Das Attributionsfenster

Definiert, wie lange nach einem Klick eine Konversion zugeschrieben werden kann. Ich nutze 30 Tage für die meisten SaaS-Produkte:

typescript
// app/lib/attribution/attribution-window.ts

export const DEFAULT_ATTRIBUTION_WINDOW_DAYS = 30;

export function isWithinAttributionWindow(
  clickedAt: Date,
  convertedAt: Date
): boolean {
  const windowMs = DEFAULT_ATTRIBUTION_WINDOW_DAYS * 24 * 60 * 60 * 1000;
  return convertedAt.getTime() - clickedAt.getTime() <= windowMs;
}

Das Attributions-Dashboard aufbauen

Das ist, was Sie wirklich sehen möchten:

Channel-Performance-Tabelle

typescript
// app/routes/admin.attribution.tsx

export const Route = createFileRoute('/admin/attribution')({
  component: AttributionDashboard,
  loader: async () => {
    const db = await getDatabase();
    
    // Nach Quelle aggregieren
    const channelStats = await db
      .select({
        source: users.firstSource,
        medium: users.firstMedium,
        users: count(users.id),
        mrr: sum(subscriptions.mrrAmount),
        trials: count(subscriptions.trialStartedAt),
        paid: count(sql`${subscriptions.status} = 'active'`),
      })
      .from(users)
      .leftJoin(subscriptions, eq(users.id, subscriptions.userId))
      .where(gte(users.createdAt, new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)))
      .groupBy(users.firstSource, users.firstMedium);
    
    return { channelStats };
  },
});

function AttributionDashboard() {
  const { channelStats } = useLoaderData({ from: Route.id });
  
  return (
    <div className="p-6">
      <h1>Attribution Dashboard</h1>
      <table>
        <thead>
          <tr>
            <th>Source</th>
            <th>Users</th>
            <th>Trial Rate</th>
            <th>Paid Rate</th>
            <th>MRR</th>
          </tr>
        </thead>
        <tbody>
          {channelStats.map(channel => (
            <tr key={channel.source}>
              <td>{channel.source || 'direct'}</td>
              <td>{channel.users}</td>
              <td>{((channel.trials / channel.users) * 100).toFixed(1)}%</td>
              <td>{((channel.paid / channel.users) * 100).toFixed(1)}%</td>
              <td>${channel.mrr?.toFixed(2) || 0}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Häufige Fallstricke

1. utm_content ignorieren

Die meisten Menschen tracken Quelle und Medium, aber überspringen Content. Ohne Content-Tracking können Sie nicht erkennen, welcher Blog-Post-CTAs Umsatz generiert haben.

2. Direct Traffic nicht erfassen

Direct Traffic (User, die Ihre URL direkt eingeben) repräsentiert oft Markenbekanntheit. Tracken Sie diese separat:

typescript
// Wenn keine UTM-Parameter und kein Referrer, ist es Direct
if (!utm.source && !request.headers.get('referer')) {
  source = 'direct';
}

3. Zu kurze Attributionsfenster

30 Tage funktionieren für die meisten B2B-SaaS. Für Consumer-Apps mit längeren Entscheidungszyklen nutzen Sie 90 Tage.

4. Keine Verbindung zu Revenue

Traffic Attribution ohne Revenue Attribution ist Eitelkeit. Stellen Sie sicher, dass jeder User-Datensatz mit seinem Subscription-Status und MRR-Beitrag verknüpft ist.


Implementierungsleitfaden

Entscheidungsrahmen: Wann UTM Attribution implementieren

SituationImplementierungsstufeZeitaufwandROI-Zeitplan
Pre-launchBasis-Tracking4 StundenVon Tag 1 an messen
Early tractionBasis + Revenue-Verbindung12 StundenErkenntnisse in 2 Wochen
Growth stageVollständiges Attributionssystem40 StundenOptimierung in 1 Monat
Scale stageMulti-Touch-Modellierung80+ StundenFortgeschrittene Erkenntnisse in 2 Monaten

Phase 1: Basis-Tracking (Woche 1)

  • UTM-Parameter-Parsing auf Ihrer Landingpage hinzufügen
  • Touchpoints in Ihrer Datenbank speichern
  • First/Last Source bei User-Signup tracken
  • Basis-Attributions-Dashboard aufbauen

Geschätzter Aufwand: 8–12 Stunden für einen Solo-Entwickler

Phase 2: Revenue-Verbindung (Woche 2–3)

  • Touchpoints mit Subscription-Events verknüpfen
  • First-Click- vs. Last-Click-Attribution implementieren
  • Channel-Performance-Metriken hinzufügen
  • MRR-nach-Channel-Dashboard erstellen

Geschätzter Aufwand: 16–24 Stunden

Phase 3: Fortgeschrittene Features (Monat 2)

  • Multi-Touch-Attributionsmodellierung
  • Content-Level-Tracking (utm_content)
  • Kampagnen-Kohortenanalyse
  • Automatisierte Alerts für Channel-Performance

Geschätzter Aufwand: 40+ Stunden

Tools und Integrationen

Analytics-Plattformen, die gut mit UTM funktionieren:

  • PostHog – Open-Source, selbst-hostbar, exzellenter UTM-Support
  • Mixpanel – Fortgeschrittene Attributionsmodellierung eingebaut
  • Amplitude – Revenue Attribution out of the box
  • GA4 – Kostenlos, aber begrenzte Attributionsfunktionen

Datenbanküberlegungen:

  • PostgreSQL/MySQL – Standard-SQL funktioniert gut für Attributionsabfragen
  • SQLite (D1) – Großartig für kleinere Apps, begrenzt für komplexe Analytics
  • ClickHouse – Für die meisten SaaS überdimensioniert, exzellent für Big Data

FAQ

Welches Attributionsmodell sollte ich verwenden?

Beginnen Sie mit Last-Click wegen der Einfachheit. Es ist nicht perfekt, aber es zeigt Ihnen, welcher Touchpoint unmittelbar vor der Konversion lag. Sobald Sie 6+ Monate Daten haben, können Sie mit First-Click- und Multi-Touch-Modellen experimentieren.

Wie gehe ich mit organischem vs. bezahltem Social um?

Verschiedene utm_medium-Werte:

  • Bezahltes Social: utm_medium=cpc oder utm_medium=paid_social
  • Organisches Social: utm_medium=social

Damit können Sie „unser Tweet ist viral gegangen" von „wir haben 500 $ für diese Ergebnisse bezahlt" unterscheiden.

Sollte ich interne Links tracken?

Nein. Fügen Sie keine UTM-Parameter zu interner Navigation hinzu. Das verunreinigt Ihre Daten. Tracken Sie interne Navigation separat über das Page-View-Tracking Ihrer Analytics-Plattform.

Was ist mit Referral-Traffic?

Referrer-Header behandeln:

typescript
const referrer = request.headers.get('referer')
if (referrer && !utm.source) {
  const domain = new URL(referrer).hostname
  source = domain.replace('www.', '')
  medium = 'referral'
}

Wie lange sollte mein Attributionsfenster sein?

30 Tage für B2B SaaS ist Standard. Für Consumer-Apps mit längeren Entscheidungszyklen (Immobilien, Enterprise-Software) nutzen Sie 90 Tage. Wichtig ist die Konsistenz – ändern Sie die Fenster nicht mitten in der Analyse.

Was, wenn ein User mehrere Touchpoints hat?

Alle Touchpoints in Ihrer Attributionstabelle speichern. Ihr Dashboard sollte zeigen:

  • First Touch – Wie sie Sie entdeckt haben
  • Last Touch – Was unmittelbar vor der Konversion lag
  • Touchpoint-Anzahl – Wie viele Interaktionen vor der Konversion

User mit 5+ Touchpoints konvertieren signifikant häufiger als User mit nur einem Touchpoint.


Nächste Schritte

Beginnen Sie mit den Grundlagen: Fügen Sie UTM-Parameter-Parsing zu Ihrer Landingpage hinzu, speichern Sie Touchpoints in Ihrer Datenbank und tracken Sie First/Last Source bei User-Signup. Das vollständige Attributionsmodell können Sie dann schrittweise aufbauen.

Für die vollständige Implementierung mit Datenbankschema, Middleware und Admin-Dashboard schauen Sie sich TanStack Ships UTM-Attributionsmodul an, das dieses gesamte System serienmäßig mitliefert.


Weiterführende Lektüre


Jeden Klick tracken. Jeden Dollar attributieren. Der Unterschied zwischen Vermuten und Wissen ist ein gut implementiertes UTM-System.