Marketing Analytics
UTM Tracking Setup
UTM Parameter Standartlari
https://example.com/landing?
utm_source=google # Trafik kaynagi (google, facebook, newsletter)
&utm_medium=cpc # Kanal tipi (cpc, email, social, organic)
&utm_campaign=spring_2026 # Kampanya adi
&utm_term=saas+analytics # Arama terimi (paid search)
&utm_content=hero_banner # Reklam varyanti (A/B test)
UTM Naming Convention
| Parameter | Format | Ornekler | |-----------|--------|---------| | source | lowercase, platform adi | google, facebook, linkedin, newsletter | | medium | lowercase, kanal tipi | cpc, cpm, email, social, organic, referral | | campaign | snake_case, tarih dahil | spring_sale_2026, product_launch_q1 | | term | + ile ayrilmis | saas+analytics, project+management | | content | snake_case, varyant | hero_banner, sidebar_cta, email_v2 |
UTM Builder (TypeScript)
interface UTMConfig {
baseUrl: string;
source: string;
medium: string;
campaign: string;
term?: string;
content?: string;
}
function buildUTMUrl(config: UTMConfig): string {
const params = new URLSearchParams();
params.set("utm_source", config.source.toLowerCase());
params.set("utm_medium", config.medium.toLowerCase());
params.set("utm_campaign", config.campaign.toLowerCase().replace(/\s+/g, "_"));
if (config.term) params.set("utm_term", config.term.toLowerCase());
if (config.content) params.set("utm_content", config.content.toLowerCase());
const separator = config.baseUrl.includes("?") ? "&" : "?";
return `${config.baseUrl}${separator}${params.toString()}`;
}
// UTM parametrelerini parse et ve kaydet
function captureUTM(): UTMParams | null {
const params = new URLSearchParams(window.location.search);
const utm: UTMParams = {
source: params.get("utm_source") || undefined,
medium: params.get("utm_medium") || undefined,
campaign: params.get("utm_campaign") || undefined,
term: params.get("utm_term") || undefined,
content: params.get("utm_content") || undefined,
};
if (utm.source) {
// First-touch ve last-touch ayri kaydet
if (!localStorage.getItem("utm_first_touch")) {
localStorage.setItem("utm_first_touch", JSON.stringify({ ...utm, timestamp: Date.now() }));
}
localStorage.setItem("utm_last_touch", JSON.stringify({ ...utm, timestamp: Date.now() }));
return utm;
}
return null;
}
Attribution Modeling
Attribution Modelleri
| Model | Aciklama | Ne Zaman Kullan | |-------|----------|----------------| | First Touch | Ilk temas %100 kredi alir | Awareness kampanyalari | | Last Touch | Son temas %100 kredi alir | Direct response kampanyalari | | Linear | Tum temaslar esit kredi alir | Tum kanallari esit degerlendirme | | Time Decay | Son temaslara daha cok kredi | Uzun satis dongusu | | U-Shaped | Ilk ve son temas %40, orta %20 | Balanced B2B attribution | | W-Shaped | Ilk, lead, opportunity %30, geri kalan %10 | Full-funnel B2B | | Data-Driven | Algoritmik (Markov chain, Shapley) | Yeterli veri varsa (10K+ conversion) |
Multi-Touch Attribution Query
-- U-Shaped Attribution
WITH touchpoints AS (
SELECT
conversion_id,
user_id,
channel,
touch_timestamp,
ROW_NUMBER() OVER (PARTITION BY conversion_id ORDER BY touch_timestamp) AS touch_order,
COUNT(*) OVER (PARTITION BY conversion_id) AS total_touches
FROM marketing_touches
WHERE conversion_id IS NOT NULL
),
attributed AS (
SELECT
conversion_id,
channel,
CASE
WHEN total_touches = 1 THEN 1.0
WHEN total_touches = 2 THEN 0.5
WHEN touch_order = 1 THEN 0.4 -- first touch
WHEN touch_order = total_touches THEN 0.4 -- last touch
ELSE 0.2 / (total_touches - 2) -- middle touches
END AS attribution_weight
FROM touchpoints
)
SELECT
channel,
ROUND(SUM(attribution_weight), 2) AS attributed_conversions,
ROUND(SUM(attribution_weight * c.revenue), 2) AS attributed_revenue
FROM attributed a
JOIN conversions c ON a.conversion_id = c.id
GROUP BY channel
ORDER BY attributed_revenue DESC;
Markov Chain Attribution
interface TransitionMatrix {
[fromState: string]: {
[toState: string]: number; // probability
};
}
// Removal effect: Her kanalin conversion'a katki oranini hesapla
function calculateRemovalEffect(
matrix: TransitionMatrix,
channels: string[]
): Record<string, number> {
const baseConversionRate = simulateConversions(matrix, channels);
const effects: Record<string, number> = {};
for (const channel of channels) {
const withoutChannel = channels.filter(c => c !== channel);
const reducedRate = simulateConversions(matrix, withoutChannel);
effects[channel] = (baseConversionRate - reducedRate) / baseConversionRate;
}
// Normalize to sum to 1
const total = Object.values(effects).reduce((a, b) => a + b, 0);
for (const channel of channels) {
effects[channel] = effects[channel] / total;
}
return effects;
}
CAC (Customer Acquisition Cost)
CAC Hesaplama
interface CACMetrics {
totalMarketingSpend: number; // Toplam marketing harcamasi
totalSalesSpend: number; // Toplam sales harcamasi (maas dahil)
newCustomers: number; // Kazanilan musteri sayisi
period: string; // "2026-Q1"
}
function calculateCAC(metrics: CACMetrics): {
blendedCAC: number;
paidCAC: number;
organicCAC: number;
} {
const totalSpend = metrics.totalMarketingSpend + metrics.totalSalesSpend;
return {
blendedCAC: totalSpend / metrics.newCustomers,
paidCAC: metrics.totalMarketingSpend / (metrics.newCustomers * 0.6), // %60 paid
organicCAC: (metrics.totalSalesSpend * 0.3) / (metrics.newCustomers * 0.4),
};
}
CAC by Channel Query
SELECT
channel,
SUM(spend) AS total_spend,
COUNT(DISTINCT conversion_user_id) AS new_customers,
ROUND(SUM(spend) / NULLIF(COUNT(DISTINCT conversion_user_id), 0), 2) AS cac,
ROUND(AVG(first_order_value), 2) AS avg_first_order
FROM (
SELECT
a.channel,
a.spend,
c.user_id AS conversion_user_id,
c.revenue AS first_order_value
FROM ad_spend a
LEFT JOIN conversions c ON c.attributed_channel = a.channel
AND c.conversion_date BETWEEN a.date AND a.date + INTERVAL '30 days'
WHERE a.date >= CURRENT_DATE - INTERVAL '90 days'
) channel_data
GROUP BY channel
ORDER BY cac;
CAC Benchmarks
| Industry | Median CAC | Iyi CAC | Target LTV:CAC | |----------|-----------|---------|---------------| | SaaS B2B (SMB) | $200-500 | < $200 | 3:1+ | | SaaS B2B (Enterprise) | $5K-20K | < $5K | 5:1+ | | SaaS B2C | $20-100 | < $30 | 3:1+ | | E-commerce | $10-50 | < $15 | 3:1+ | | Fintech | $100-500 | < $100 | 4:1+ | | Marketplace | $50-200 | < $50 | 3:1+ |
ROAS (Return on Ad Spend)
ROAS Calculator
function calculateROAS(
revenue: number,
adSpend: number
): { roas: number; roasPercentage: number; profitable: boolean } {
const roas = revenue / adSpend;
return {
roas: Math.round(roas * 100) / 100,
roasPercentage: Math.round(roas * 100),
profitable: roas > 1,
};
}
// Hedef ROAS hesapla (break-even icin)
function targetROAS(grossMargin: number): number {
// Minimum ROAS = 1 / Gross Margin
// %70 margin -> minimum 1.43 ROAS
return Math.round((1 / grossMargin) * 100) / 100;
}
ROAS Dashboard Query
SELECT
campaign_name,
channel,
SUM(impressions) AS impressions,
SUM(clicks) AS clicks,
ROUND(100.0 * SUM(clicks) / NULLIF(SUM(impressions), 0), 2) AS ctr_pct,
SUM(spend) AS spend,
SUM(conversions) AS conversions,
ROUND(SUM(spend) / NUL