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
| Parameter | Zweck | Beispiel |
|---|---|---|
utm_source | Woher der Traffic kam | twitter, linkedin, product_hunt |
utm_medium | Das Marketingmedium | social, email, cpc, referral |
utm_campaign | Die spezifische Kampagne | q3_launch, summer_promo, indie_hackers_launch |
utm_term | Bezahlte Suchbegriffe | saas_boilerplate, tanstack_start |
utm_content | Spezifische Content-Variante | hero_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
// 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:
// 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:
// 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:
// 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:
-- 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:
// 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
// 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:
// 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
| Situation | Implementierungsstufe | Zeitaufwand | ROI-Zeitplan |
|---|---|---|---|
| Pre-launch | Basis-Tracking | 4 Stunden | Von Tag 1 an messen |
| Early traction | Basis + Revenue-Verbindung | 12 Stunden | Erkenntnisse in 2 Wochen |
| Growth stage | Vollständiges Attributionssystem | 40 Stunden | Optimierung in 1 Monat |
| Scale stage | Multi-Touch-Modellierung | 80+ Stunden | Fortgeschrittene 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=cpcoderutm_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:
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
- SaaS Growth Framework – Vollständiger Growth-Operations-Stack
- Marketing Psychology – Verstehen, was Konversionen antreibt
- PostHog Setup Guide – Best Practices für die Analytics-Implementierung
Jeden Klick tracken. Jeden Dollar attributieren. Der Unterschied zwischen Vermuten und Wissen ist ein gut implementiertes UTM-System.