Attribution Modeling Beyond Last-Click — What DTC Brands Actually Need
The Last-Click Lie
A DTC brand spent $500K/month on marketing. According to last-click attribution: Google Brand Search drove 60% of conversions. Facebook drove 5%. They cut Facebook spend by 50%. Two weeks later, Google Brand Search conversions dropped 35%.
Why? Facebook drove awareness. Customers saw Facebook ads, remembered the brand, then Googled the brand name and bought. Last-click gave all credit to Google. When they cut Facebook, fewer people searched for the brand.
Last-click attribution is the default in every analytics platform because it's simple. It's also dangerously wrong for any business with a multi-touch customer journey.
Attribution Models Explained
Last-Click:
Touchpoints: Facebook Ad → Blog Post → Google Search → Purchase
Credit: 0% 0% 100% ←
Problem: Ignores everything that led to the search
First-Click:
Touchpoints: Facebook Ad → Blog Post → Google Search → Purchase
Credit: 100% 0% 0% ←
Problem: Ignores everything after initial awareness
Linear:
Touchpoints: Facebook Ad → Blog Post → Google Search → Purchase
Credit: 33% 33% 33% ←
Better: At least acknowledges every touchpoint
Problem: Treats all touchpoints as equally important
Time-Decay:
Touchpoints: Facebook Ad → Blog Post → Google Search → Purchase
Credit: 15% 25% 60% ←
Better: Gives more credit to recent touchpoints
Problem: Still under-credits top-of-funnel
Position-Based (U-Shaped):
Touchpoints: Facebook Ad → Blog Post → Google Search → Purchase
Credit: 40% 20% 40% ←
Best default: Credits both discovery and conversion touchpoints
Implementing Multi-Touch Attribution
Step 1: Capture Every Touchpoint
// Track all marketing touchpoints in a customer journey
interface Touchpoint {
timestamp: Date;
channel: string; // "facebook", "google", "email", "direct"
campaign?: string; // UTM campaign
medium: string; // "cpc", "organic", "social", "email"
source: string; // "google", "facebook", "klaviyo"
landingPage: string;
sessionId: string;
userId?: string; // If logged in
anonymousId: string; // Cookie-based ID
}
// Capture on every page load
function captureTouchpoint() {
const params = new URLSearchParams(window.location.search);
const referrer = document.referrer;
const touchpoint: Touchpoint = {
timestamp: new Date(),
channel: determineChannel(params, referrer),
campaign: params.get("utm_campaign") || undefined,
medium: params.get("utm_medium") || inferMedium(referrer),
source: params.get("utm_source") || inferSource(referrer),
landingPage: window.location.pathname,
sessionId: getSessionId(),
anonymousId: getAnonymousId(),
};
// Store in first-party storage (not just cookies — those get blocked)
appendToJourney(touchpoint);
}Step 2: Build the Customer Journey
// Reconstruct the full journey at conversion time
async function getCustomerJourney(customerId: string): Promise<Touchpoint[]> {
// Merge anonymous and authenticated touchpoints
const anonymousId = getAnonymousIdForCustomer(customerId);
const touchpoints = await db.touchpoints.findMany({
where: {
OR: [
{ userId: customerId },
{ anonymousId: anonymousId },
],
},
orderBy: { timestamp: "asc" },
});
// Deduplicate and clean
return deduplicateTouchpoints(touchpoints);
}Step 3: Apply Attribution Models
function attributeConversion(
journey: Touchpoint[],
conversionValue: number,
model: "linear" | "time_decay" | "position_based" = "position_based"
): AttributionResult[] {
if (journey.length === 0) return [];
if (journey.length === 1) {
return [{ touchpoint: journey[0], credit: conversionValue }];
}
switch (model) {
case "linear":
const equalCredit = conversionValue / journey.length;
return journey.map(tp => ({ touchpoint: tp, credit: equalCredit }));
case "time_decay": {
const halfLife = 7 * 24 * 60 * 60 * 1000; // 7 days
const conversionTime = journey[journey.length - 1].timestamp.getTime();
const weights = journey.map(tp => {
const age = conversionTime - tp.timestamp.getTime();
return Math.pow(0.5, age / halfLife);
});
const totalWeight = weights.reduce((a, b) => a + b, 0);
return journey.map((tp, i) => ({
touchpoint: tp,
credit: (weights[i] / totalWeight) * conversionValue,
}));
}
case "position_based": {
// 40% first, 40% last, 20% distributed among middle
const credits = journey.map((_, i) => {
if (i === 0) return 0.4;
if (i === journey.length - 1) return 0.4;
return 0.2 / (journey.length - 2);
});
return journey.map((tp, i) => ({
touchpoint: tp,
credit: credits[i] * conversionValue,
}));
}
}
}The Attribution Dashboard
Channel Performance (Position-Based Attribution):
| Channel | Last-Click Rev | Multi-Touch Rev | Difference |
|----------------|---------------|-----------------|------------|
| Google Brand | $300K (60%) | $150K (30%) | -50% |
| Facebook Ads | $25K (5%) | $125K (25%) | +400% |
| Email | $100K (20%) | $80K (16%) | -20% |
| Google Non-Brand| $50K (10%) | $75K (15%) | +50% |
| Direct | $25K (5%) | $70K (14%) | +180% |
Insight: Facebook is 5x more valuable than last-click suggests.
Cutting Facebook spend would reduce Google Brand conversions
because Facebook drives the awareness that creates brand searches.
Privacy-Compliant Attribution
With third-party cookies dying, attribution needs to adapt:
First-party data strategies:
→ Server-side tracking (events sent from your server, not browser)
→ First-party cookies (your domain, longer lifespan)
→ Authenticated user matching (email-based cross-device)
→ Media Mix Modeling (statistical, no user-level tracking needed)
Implementation:
→ Move tracking server-side (Meta CAPI, Google Enhanced Conversions)
→ Encourage account creation earlier in the funnel
→ Use post-purchase surveys: "How did you hear about us?"
→ Supplement with MMM for channel-level budget allocation
Multi-touch attribution isn't just a reporting change — it's a budget allocation change. When you see the real contribution of each channel, you spend differently. And when you spend differently based on better data, you grow faster.