Conversion Lag Report
Schat per conversieactie hoe lang conversies na de klik binnenkomen door conversies op klikdatum en op conversiedatum te vergelijken, met 50/80/95%-punten en advies.
Wat dit script doet
- Leest per conversieactie de instellingen (categorie, teltype, click-through lookback window, telt mee in Conversies).
- Haalt per dag en conversieactie metrics.conversions en all_conversions op, zowel op klikdatum als by_conversion_date.
- Gebruikt de oudere, volledige periode als referentie en berekent voor recente dagen de vulgraad.
- Leidt daaruit af na hoeveel dagen ongeveer 50%, 80% en 95% van de conversies geregistreerd is en de gemiddelde lag.
- Schrijft een samenvatting met advies en een dagtabel naar de Sheet en mailt optioneel.
Broncode
1/**2 * Conversion Lag Report3 *4 * Schat per conversieactie de conversievertraging (de tijd tussen klik en conversie) door5 * conversies toegerekend aan de klikdatum (metrics.conversions / all_conversions) te6 * vergelijken met conversies toegerekend aan de conversiedatum7 * (metrics.conversions_by_conversion_date / all_conversions_by_conversion_date) per dag.8 * Voor recente dagen ligt het aantal "op klikdatum" structureel lager zolang er nog9 * conversies binnenkomen; uit dat verloop wordt afgeleid na hoeveel dagen ~50%, ~80% en10 * ~95% van de conversies is geregistreerd. Rapport gaat naar een Google Sheet en optioneel11 * per e-mail. Het script wijzigt niets in het account.12 *13 * Versie: 1.2.014 * Frequentie: Wekelijks15 * Niveau: Account16 * Licentie: MIT17 */1819// ===== CONFIGURATIE =====2021// Google Sheet voor het rapport.22var SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/d/JOUW_SHEET_ID/edit';23var SHEET_NAMES = { summary: 'Conversielag', daily: 'Conversielag per dag' };2425// Aantal dagen historie. Minimaal 60 aanbevolen: de oudste 30 dagen dienen als "volledig" referentiekader.26var DAYS_OF_HISTORY = 90;2728// Aantal meest recente dagen dat als "nog niet volledig" wordt beschouwd bij het schatten van de lag.29var RECENT_WINDOW_DAYS = 30;3031// Alleen conversieacties met minimaal dit aantal conversies in de hele periode.32var MIN_CONVERSIONS = 20;3334// Optioneel e-mailoverzicht. Leeg = geen e-mail.35var EMAIL_RECIPIENTS = '';3637// ===== EINDE CONFIGURATIE =====3839function main() {40 try {41 var account = AdsApp.currentAccount();42 var timezone = account.getTimeZone();43 var today = new Date();44 var endDate = fmt(addDays(today, -1), timezone);45 var startDate = fmt(addDays(today, -DAYS_OF_HISTORY), timezone);46 Logger.log('Conversielag-rapport voor ' + account.getName() + ' (' + startDate + ' t/m ' + endDate + ')');4748 var actionsMeta = fetchConversionActions();49 var daily = fetchDailyByAction(startDate, endDate);50 var summary = estimateLag(daily, actionsMeta, endDate, timezone);51 Logger.log(summary.length + ' conversieacties geanalyseerd.');52 summary.forEach(function (s) {53 Logger.log(s.name + ': ' + s.totalByConvDate.toFixed(1) + ' conv. | gem. lag ~' + (s.avgLagDays === null ? '?' : s.avgLagDays.toFixed(1) + ' dagen') +54 ' | 80% binnen ' + (s.p80Days === null ? '?' : s.p80Days + ' dagen') + ' | lookback ' + s.lookbackDays + 'd');55 });5657 var ss = SpreadsheetApp.openByUrl(SPREADSHEET_URL);58 writeSummary(ss, summary, startDate, endDate);59 writeDaily(ss, daily, actionsMeta);60 sendEmail(account, summary);61 Logger.log('Klaar: ' + SPREADSHEET_URL);62 } catch (e) {63 Logger.log('FOUT in conversion-lag-report: ' + e + (e.stack ? '\n' + e.stack : ''));64 throw e;65 }66}6768/**69 * Metadata per conversieactie (lookback window, categorie, telt mee in "Conversies"?).70 */71function fetchConversionActions() {72 var query =73 'SELECT conversion_action.id, conversion_action.name, conversion_action.category, conversion_action.type, ' +74 'conversion_action.status, conversion_action.include_in_conversions_metric, conversion_action.click_through_lookback_window_days, ' +75 'conversion_action.counting_type ' +76 'FROM conversion_action ' +77 "WHERE conversion_action.status = 'ENABLED'";78 var meta = {};79 var rows = AdsApp.report(query).rows();80 while (rows.hasNext()) {81 var row = rows.next();82 meta[row['conversion_action.name']] = {83 id: row['conversion_action.id'],84 name: row['conversion_action.name'],85 category: row['conversion_action.category'],86 type: row['conversion_action.type'],87 includeInConversions: String(row['conversion_action.include_in_conversions_metric']) === 'true',88 lookbackDays: Number(row['conversion_action.click_through_lookback_window_days']),89 countingType: row['conversion_action.counting_type']90 };91 }92 return meta;93}9495/**96 * Dagelijkse conversies per conversieactie: op klikdatum en op conversiedatum.97 * Bij segmentatie op conversion_action_name zijn alleen conversie-metrics toegestaan (geen kosten).98 */99function fetchDailyByAction(startDate, endDate) {100 var query =101 'SELECT segments.date, segments.conversion_action_name, ' +102 'metrics.all_conversions, metrics.all_conversions_by_conversion_date, ' +103 'metrics.conversions, metrics.conversions_by_conversion_date, ' +104 'metrics.all_conversions_value, metrics.all_conversions_value_by_conversion_date ' +105 'FROM campaign ' +106 "WHERE segments.date BETWEEN '" + startDate + "' AND '" + endDate + "'";107 var daily = {}; // daily[action][date] = {...}108 var rows = AdsApp.report(query).rows();109 while (rows.hasNext()) {110 var row = rows.next();111 var action = row['segments.conversion_action_name'];112 var date = row['segments.date'];113 if (!action) { continue; }114 if (!daily[action]) { daily[action] = {}; }115 if (!daily[action][date]) {116 daily[action][date] = { byClick: 0, byConv: 0, byClickPrimary: 0, byConvPrimary: 0, valueByClick: 0, valueByConv: 0 };117 }118 var d = daily[action][date];119 d.byClick += Number(row['metrics.all_conversions'] || 0);120 d.byConv += Number(row['metrics.all_conversions_by_conversion_date'] || 0);121 d.byClickPrimary += Number(row['metrics.conversions'] || 0);122 d.byConvPrimary += Number(row['metrics.conversions_by_conversion_date'] || 0);123 d.valueByClick += Number(row['metrics.all_conversions_value'] || 0);124 d.valueByConv += Number(row['metrics.all_conversions_value_by_conversion_date'] || 0);125 }126 return daily;127}128129/**130 * Schat de conversielag per actie.131 * Aanpak: voor de oudste (volledige) periode is de verhouding klikdatum/conversiedatum ~1.132 * Voor elke recente dag d (x dagen geleden) is de "vulgraad" = byClick(d) / verwachte volledige waarde,133 * waarbij de verwachte volledige waarde het gemiddelde is van byConv over de referentieperiode134 * (byConv is niet afhankelijk van lag). Uit de vulgraad per aantal dagen geleden volgt de curve.135 */136function estimateLag(daily, actionsMeta, endDate, timezone) {137 var summary = [];138 var end = parseDate(endDate);139140 Object.keys(daily).forEach(function (action) {141 var days = daily[action];142 var dates = Object.keys(days).sort();143 var totalByConv = 0;144 var totalByClick = 0;145 dates.forEach(function (d) { totalByConv += days[d].byConv; totalByClick += days[d].byClick; });146 if (totalByConv < MIN_CONVERSIONS) { return; }147148 // Referentie: dagen ouder dan RECENT_WINDOW_DAYS. Gemiddelde dagwaarde op klikdatum (compleet) en op conversiedatum.149 var refClick = 0;150 var refConv = 0;151 var refDays = 0;152 var fill = []; // {daysAgo, ratio}153 dates.forEach(function (d) {154 var daysAgo = Math.round((end.getTime() - parseDate(d).getTime()) / 86400000) + 1;155 if (daysAgo > RECENT_WINDOW_DAYS) {156 refClick += days[d].byClick;157 refConv += days[d].byConv;158 refDays++;159 }160 });161 var refDailyComplete = refDays > 0 ? refClick / refDays : 0;162 // Structurele verhouding klik/conv-datum in de volledige periode (compenseert voor volumetrends).163 var structuralRatio = refConv > 0 ? refClick / refConv : 1;164165 if (refDailyComplete > 0) {166 dates.forEach(function (d) {167 var daysAgo = Math.round((end.getTime() - parseDate(d).getTime()) / 86400000) + 1;168 if (daysAgo <= RECENT_WINDOW_DAYS) {169 // Vulgraad van deze klikdag t.o.v. het verwachte complete niveau. Gebruik het weekdaggemiddelde170 // benaderd door het referentiegemiddelde; begrens op 1.171 var ratio = Math.min(days[d].byClick / refDailyComplete, 1.2);172 fill.push({ daysAgo: daysAgo, ratio: ratio });173 }174 });175 }176177 // Maak de curve monotoon (cumulatieve fractie stijgt met daysAgo) door glijdend maximum vanaf dag 1.178 fill.sort(function (a, b) { return a.daysAgo - b.daysAgo; });179 var smoothed = [];180 var running = 0;181 fill.forEach(function (f) {182 running = Math.max(running, Math.min(f.ratio, 1));183 smoothed.push({ daysAgo: f.daysAgo, fraction: running });184 });185186 var p50 = firstDayReaching(smoothed, 0.5);187 var p80 = firstDayReaching(smoothed, 0.8);188 var p95 = firstDayReaching(smoothed, 0.95);189 var avgLag = estimateAverageLag(smoothed);190191 var meta = actionsMeta[action] || {};192 var recentByClick = 0;193 var recentByConv = 0;194 dates.forEach(function (d) {195 var daysAgo = Math.round((end.getTime() - parseDate(d).getTime()) / 86400000) + 1;196 if (daysAgo <= 7) { recentByClick += days[d].byClick; recentByConv += days[d].byConv; }197 });198199 summary.push({200 name: action,201 category: meta.category || '',202 includeInConversions: meta.includeInConversions === undefined ? '' : (meta.includeInConversions ? 'Ja' : 'Nee'),203 lookbackDays: meta.lookbackDays || '',204 countingType: meta.countingType || '',205 totalByConvDate: totalByConv,206 totalByClickDate: totalByClick,207 structuralRatio: structuralRatio,208 last7ByClick: recentByClick,209 last7ByConv: recentByConv,210 last7Fill: recentByConv > 0 ? recentByClick / recentByConv : null,211 p50Days: p50,212 p80Days: p80,213 p95Days: p95,214 avgLagDays: avgLag,215 advice: buildAdvice(p80, p95, meta.lookbackDays)216 });217 });218 summary.sort(function (a, b) { return b.totalByConvDate - a.totalByConvDate; });219 return summary;220}221222function firstDayReaching(curve, target) {223 for (var i = 0; i < curve.length; i++) {224 if (curve[i].fraction >= target) { return curve[i].daysAgo; }225 }226 return null;227}228229/**230 * Gemiddelde lag = som over dagen van (1 - cumulatieve fractie), begrensd op het venster.231 */232function estimateAverageLag(curve) {233 if (!curve.length) { return null; }234 var sum = 0;235 var lastFraction = 0;236 for (var day = 1; day <= RECENT_WINDOW_DAYS; day++) {237 var point = null;238 for (var i = 0; i < curve.length; i++) { if (curve[i].daysAgo === day) { point = curve[i]; break; } }239 if (point) { lastFraction = point.fraction; }240 sum += (1 - lastFraction);241 }242 return Math.max(sum, 0);243}244245function buildAdvice(p80, p95, lookbackDays) {246 if (p80 === null) { return 'Onvoldoende data voor een schatting.'; }247 var parts = [];248 if (p80 <= 1) { parts.push('Nauwelijks lag: prestaties van gisteren zijn vrijwel compleet.'); }249 else if (p80 <= 7) { parts.push('Wacht ~' + p80 + ' dagen voordat je een periode beoordeelt (80% compleet).'); }250 else { parts.push('Lange lag: wacht minimaal ' + p80 + ' dagen (80%) en ' + (p95 || RECENT_WINDOW_DAYS + '+') + ' dagen (95%) voordat je op deze conversie optimaliseert.'); }251 if (lookbackDays && p95 !== null && p95 >= lookbackDays * 0.8) {252 parts.push('Conversievenster (' + lookbackDays + 'd) lijkt krap; overweeg een langer click-through venster.');253 }254 if (p80 > 7) { parts.push('Gebruik in Smart Bidding een langere evaluatieperiode en vermijd dagelijkse tROAS/tCPA-aanpassingen.'); }255 return parts.join(' ');256}257258function writeSummary(ss, summary, startDate, endDate) {259 var sheet = ss.getSheetByName(SHEET_NAMES.summary) || ss.insertSheet(SHEET_NAMES.summary);260 sheet.clearContents();261 var header = ['Conversieactie', 'Categorie', 'Telt in Conversies', 'Teltype', 'Lookback (d)', 'Alle conv. (conversiedatum)', 'Alle conv. (klikdatum)',262 'Verhouding klik/conv-datum (ref.)', 'Laatste 7d op klikdatum', 'Laatste 7d op conversiedatum', 'Vulgraad laatste 7d', 'Gem. lag (dagen)', '50% binnen (d)', '80% binnen (d)', '95% binnen (d)', 'Advies'];263 var values = [header].concat(summary.map(function (s) {264 return [s.name, s.category, s.includeInConversions, s.countingType, s.lookbackDays, round(s.totalByConvDate), round(s.totalByClickDate), round(s.structuralRatio),265 round(s.last7ByClick), round(s.last7ByConv), s.last7Fill === null ? '' : s.last7Fill, s.avgLagDays === null ? '' : round(s.avgLagDays),266 s.p50Days === null ? '' : s.p50Days, s.p80Days === null ? '' : s.p80Days, s.p95Days === null ? '' : s.p95Days, s.advice];267 }));268 sheet.getRange(1, 1, values.length, header.length).setValues(values);269 sheet.getRange(1, 1, 1, header.length).setFontWeight('bold').setBackground('#e8eaf6');270 sheet.setFrozenRows(1);271 if (values.length > 1) { sheet.getRange(2, 11, values.length - 1, 1).setNumberFormat('0%'); }272 sheet.getRange(1, header.length + 2).setValue('Periode ' + startDate + ' t/m ' + endDate + ' | bijgewerkt ' + new Date());273}274275function writeDaily(ss, daily, actionsMeta) {276 var sheet = ss.getSheetByName(SHEET_NAMES.daily) || ss.insertSheet(SHEET_NAMES.daily);277 sheet.clearContents();278 var header = ['Datum', 'Conversieactie', 'Alle conv. (klikdatum)', 'Alle conv. (conversiedatum)', 'Conversies (klikdatum)', 'Conversies (conversiedatum)', 'Waarde (klikdatum)', 'Waarde (conversiedatum)'];279 var values = [header];280 Object.keys(daily).sort().forEach(function (action) {281 Object.keys(daily[action]).sort().forEach(function (date) {282 var d = daily[action][date];283 if (d.byClick === 0 && d.byConv === 0) { return; }284 values.push([date, action, round(d.byClick), round(d.byConv), round(d.byClickPrimary), round(d.byConvPrimary), round(d.valueByClick), round(d.valueByConv)]);285 });286 });287 sheet.getRange(1, 1, values.length, header.length).setValues(values);288 sheet.getRange(1, 1, 1, header.length).setFontWeight('bold').setBackground('#e8eaf6');289 sheet.setFrozenRows(1);290}291292function sendEmail(account, summary) {293 if (!EMAIL_RECIPIENTS || !summary.length) { return; }294 var html = '<div style="font-family:Arial,sans-serif;font-size:13px"><h3>Conversielag ' + escapeHtml(account.getName()) + '</h3>' +295 '<table cellpadding="5" style="border-collapse:collapse;border:1px solid #ddd"><tr style="background:#f5f5f5"><th align="left">Conversieactie</th><th>Conv.</th><th>Gem. lag</th><th>80% binnen</th><th>95% binnen</th><th align="left">Advies</th></tr>';296 summary.forEach(function (s) {297 html += '<tr><td>' + escapeHtml(s.name) + '</td><td align="right">' + s.totalByConvDate.toFixed(0) + '</td><td align="right">' + (s.avgLagDays === null ? '-' : s.avgLagDays.toFixed(1) + 'd') +298 '</td><td align="right">' + (s.p80Days === null ? '-' : s.p80Days + 'd') + '</td><td align="right">' + (s.p95Days === null ? '-' : s.p95Days + 'd') + '</td><td>' + escapeHtml(s.advice) + '</td></tr>';299 });300 html += '</table><p><a href="' + SPREADSHEET_URL + '">Open de Sheet</a></p></div>';301 try {302 MailApp.sendEmail({ to: EMAIL_RECIPIENTS, subject: '[Google Ads] Conversielag-rapport - ' + account.getName(), htmlBody: html });303 } catch (e) {304 Logger.log('E-mail versturen mislukt: ' + e);305 }306}307308function parseDate(str) {309 var p = str.split('-');310 return new Date(Number(p[0]), Number(p[1]) - 1, Number(p[2]));311}312313function addDays(date, days) {314 var d = new Date(date.getTime());315 d.setDate(d.getDate() + days);316 return d;317}318319function fmt(date, timezone) {320 return Utilities.formatDate(date, timezone, 'yyyy-MM-dd');321}322323function round(value) {324 return Math.round(Number(value) * 100) / 100;325}326327function escapeHtml(text) {328 return String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');329}330Installatie & configuratie
Installatie
- Plak de URL van een Google Sheet in SPREADSHEET_URL.
- Stel DAYS_OF_HISTORY in (standaard 90). De oudste dagen buiten RECENT_WINDOW_DAYS (standaard 30) dienen als referentie voor wat een volledige dag is.
- Stel MIN_CONVERSIONS in om conversieacties met te weinig volume over te slaan.
- Optioneel: EMAIL_RECIPIENTS.
Testen
Draai een preview. In de logs staat per conversieactie het totaal, de geschatte gemiddelde lag, de dag waarop 80% bereikt is en het ingestelde lookback-venster. Vergelijk met je eigen kennis van de klantreis: een leadformulier heeft doorgaans een lag van 0-1 dag, een offerte-aanvraag of aankoop van een duur product enkele dagen tot weken.
Output
- Conversielag: per conversieactie categorie, teltype, lookback, totalen op klik- en conversiedatum, vulgraad van de laatste 7 dagen, gemiddelde lag, 50/80/95%-punten en een tekstueel advies.
- Conversielag per dag: de onderliggende dagcijfers op klikdatum en conversiedatum, ook voor conversiewaarde.
Waarom dit belangrijk is
Als 80% van je conversies pas na 5 dagen binnen is, zijn de cijfers van de laatste week structureel te laag. Beoordeel campagnes dan pas na die periode en pas tCPA/tROAS niet dagelijks aan. Een 95%-punt dat dicht bij het lookback-venster ligt betekent dat je conversies mist; verleng dan het click-through venster van de conversieactie.
De schatting is een benadering op basis van dagniveaus en werkt het best bij een stabiel dagvolume.