Sync displayed KPI events from current campaign config
This commit is contained in:
+3
-2
@@ -41,7 +41,7 @@
|
|||||||
<section class="panel table-panel"><div class="table-head"><div class="title" style="margin:0">Detail</div><div class="table-note" id="count"></div></div><div class="table-wrap"><table><thead><tr><th>Date</th><th>组别</th><th>Campaign name</th><th>KPI Event</th><th class="num">Event</th><th class="num">Spend</th><th class="num">CAC</th><th class="num">Impression</th><th class="num">Click</th><th class="num">Conversions</th><th class="num">CTR</th><th class="num">CVR</th><th class="num">CPI</th></tr></thead><tbody id="body"></tbody></table></div></section>
|
<section class="panel table-panel"><div class="table-head"><div class="title" style="margin:0">Detail</div><div class="table-note" id="count"></div></div><div class="table-wrap"><table><thead><tr><th>Date</th><th>组别</th><th>Campaign name</th><th>KPI Event</th><th class="num">Event</th><th class="num">Spend</th><th class="num">CAC</th><th class="num">Impression</th><th class="num">Click</th><th class="num">Conversions</th><th class="num">CTR</th><th class="num">CVR</th><th class="num">CPI</th></tr></thead><tbody id="body"></tbody></table></div></section>
|
||||||
<div class="foot">数据源:Tableau · DSP Daily Data。订单状态以 Campaign info 的 G 列为准,KPI Event 以 I 列为准;I 列为“-”时不追踪事件质量。实验组通过 Campaign name 中的 adping ID 识别;同一 pkg、GEO、Platform 下,其余 Campaign 合并为对照组并显示为 General campaign。CAC = Spend / KPI Event,CPI = Spend / Install;比率均使用汇总后的分子与分母重新计算。</div>
|
<div class="foot">数据源:Tableau · DSP Daily Data。订单状态以 Campaign info 的 G 列为准,KPI Event 以 I 列为准;I 列为“-”时不追踪事件质量。实验组通过 Campaign name 中的 adping ID 识别;同一 pkg、GEO、Platform 下,其余 Campaign 合并为对照组并显示为 General campaign。CAC = Spend / KPI Event,CPI = Spend / Install;比率均使用汇总后的分子与分母重新计算。</div>
|
||||||
</main><script>
|
</main><script>
|
||||||
const $=id=>document.getElementById(id); let all=[],lastRows=[],trendVisible={spend:true,conversions:true};
|
const $=id=>document.getElementById(id); let all=[],campaignConfig=[],lastRows=[],trendVisible={spend:true,conversions:true};
|
||||||
const n=(v,d=0)=>Number(v||0).toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d});
|
const n=(v,d=0)=>Number(v||0).toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d});
|
||||||
const rate=(a,b)=>b?a/b:0, pct=v=>(100*v).toFixed(2)+'%', money=v=>'$'+n(v,2);
|
const rate=(a,b)=>b?a/b:0, pct=v=>(100*v).toFixed(2)+'%', money=v=>'$'+n(v,2);
|
||||||
function selected(id){return [...$(id).selectedOptions].map(o=>o.value)}
|
function selected(id){return [...$(id).selectedOptions].map(o=>o.value)}
|
||||||
@@ -52,6 +52,7 @@ function updateDateBounds(reset=false){const months=selected('month'),rows=ancho
|
|||||||
function updateAnchoredFilters(resetDates=false){const rows=anchorRows();multiOptions('month',rows.map(r=>r.date.slice(0,7)));singleOptions('geo',rows.map(r=>r.geo));singleOptions('source',rows.map(r=>r.source));multiOptions('group',rows.map(r=>r.group));updateDateBounds(resetDates)}
|
function updateAnchoredFilters(resetDates=false){const rows=anchorRows();multiOptions('month',rows.map(r=>r.date.slice(0,7)));singleOptions('geo',rows.map(r=>r.geo));singleOptions('source',rows.map(r=>r.source));multiOptions('group',rows.map(r=>r.group));updateDateBounds(resetDates)}
|
||||||
function filtered(){const q=$('search').value.trim().toLowerCase(),months=selected('month'),groups=selected('group');return all.filter(r=>r.status==='Active'&&(!$('pkg').value||r.pkg===$('pkg').value)&&(!months.length||months.includes(r.date.slice(0,7)))&&(!$('geo').value||r.geo===$('geo').value)&&(!$('source').value||r.source===$('source').value)&&(!groups.length||groups.includes(r.group))&&(!$('start').value||r.date>=$('start').value)&&(!$('end').value||r.date<=$('end').value)&&(!q||(`${r.campaign} ${r.adpingId}`).toLowerCase().includes(q)))}
|
function filtered(){const q=$('search').value.trim().toLowerCase(),months=selected('month'),groups=selected('group');return all.filter(r=>r.status==='Active'&&(!$('pkg').value||r.pkg===$('pkg').value)&&(!months.length||months.includes(r.date.slice(0,7)))&&(!$('geo').value||r.geo===$('geo').value)&&(!$('source').value||r.source===$('source').value)&&(!groups.length||groups.includes(r.group))&&(!$('start').value||r.date>=$('start').value)&&(!$('end').value||r.date<=$('end').value)&&(!q||(`${r.campaign} ${r.adpingId}`).toLowerCase().includes(q)))}
|
||||||
const tracksEvent=r=>{const k=String(r.kpi||'').trim();return k&&k!=='-'};
|
const tracksEvent=r=>{const k=String(r.kpi||'').trim();return k&&k!=='-'};
|
||||||
|
function latestKpi(r){const matches=campaignConfig.filter(c=>String(c.status).toLowerCase()==='active'&&c.pkg===r.pkg&&String(c.channel).toLowerCase()===r.source&&String(c.geo).toUpperCase()===String(r.geo).toUpperCase());if(r.group==='实验组'){const hit=matches.find(c=>c.group==='实验组'&&String(c.adping_id)===String(r.adpingId));return hit?String(hit.kpi||'-').trim()||'-':r.kpi}const names=[...new Set(matches.filter(c=>c.group==='对照组').map(c=>String(c.kpi||'-').trim()||'-'))];return names.length===1?names[0]:r.kpi}
|
||||||
function eventLabel(rows){const names=[...new Set(rows.filter(tracksEvent).map(r=>String(r.kpi).trim()))];return names.length?names.join(' / '):'KPI Event'}
|
function eventLabel(rows){const names=[...new Set(rows.filter(tracksEvent).map(r=>String(r.kpi).trim()))];return names.length?names.join(' / '):'KPI Event'}
|
||||||
function aggregate(rows,keys){const m=new Map;for(const r of rows){const k=keys.map(x=>r[x]).join('\u0001');if(!m.has(k))m.set(k,{...Object.fromEntries(keys.map(x=>[x,r[x]])),impression:0,click:0,conversions:0,spend:0,event:0,eventSpend:0,eventKnown:false,kpi:r.kpi||''});const o=m.get(k);for(const x of ['impression','click','conversions','spend'])o[x]+=+r[x]||0;if(r.event!==null&&r.event!==undefined&&r.event!==''){o.event+=+r.event||0;o.eventSpend+=+r.spend||0;o.eventKnown=true}}return [...m.values()]}
|
function aggregate(rows,keys){const m=new Map;for(const r of rows){const k=keys.map(x=>r[x]).join('\u0001');if(!m.has(k))m.set(k,{...Object.fromEntries(keys.map(x=>[x,r[x]])),impression:0,click:0,conversions:0,spend:0,event:0,eventSpend:0,eventKnown:false,kpi:r.kpi||''});const o=m.get(k);for(const x of ['impression','click','conversions','spend'])o[x]+=+r[x]||0;if(r.event!==null&&r.event!==undefined&&r.event!==''){o.event+=+r.event||0;o.eventSpend+=+r.spend||0;o.eventKnown=true}}return [...m.values()]}
|
||||||
function cards(rows){const t=aggregate(rows,[])[0]||{impression:0,click:0,conversions:0,spend:0},e=aggregate(rows.filter(tracksEvent),[])[0]||{event:0,eventKnown:false,eventSpend:0},eventName=eventLabel(rows);const vals=[['Spend',money(t.spend)], [eventName,e.eventKnown?n(e.event):'—'],['CAC',e.eventKnown&&e.event?money(e.eventSpend/e.event):'—'],['Conversions',n(t.conversions)],['CPI',t.conversions?money(t.spend/t.conversions):'—'],['Impression',n(t.impression)],['Click',n(t.click)],['CTR',pct(rate(t.click,t.impression))],['CVR',pct(rate(t.conversions,t.click))]];$('cards').innerHTML=vals.map(x=>`<div class="panel card"><div class="label" title="${x[0]}">${x[0]}</div><div class="value">${x[1]}</div></div>`).join('')}
|
function cards(rows){const t=aggregate(rows,[])[0]||{impression:0,click:0,conversions:0,spend:0},e=aggregate(rows.filter(tracksEvent),[])[0]||{event:0,eventKnown:false,eventSpend:0},eventName=eventLabel(rows);const vals=[['Spend',money(t.spend)], [eventName,e.eventKnown?n(e.event):'—'],['CAC',e.eventKnown&&e.event?money(e.eventSpend/e.event):'—'],['Conversions',n(t.conversions)],['CPI',t.conversions?money(t.spend/t.conversions):'—'],['Impression',n(t.impression)],['Click',n(t.click)],['CTR',pct(rate(t.click,t.impression))],['CVR',pct(rate(t.conversions,t.click))]];$('cards').innerHTML=vals.map(x=>`<div class="panel card"><div class="label" title="${x[0]}">${x[0]}</div><div class="value">${x[1]}</div></div>`).join('')}
|
||||||
@@ -61,5 +62,5 @@ function table(rows){const d=aggregate(rows,['date','group','campaign','kpi']).s
|
|||||||
function render(){lastRows=filtered();cards(lastRows);trend(lastRows);table(lastRows)}
|
function render(){lastRows=filtered();cards(lastRows);trend(lastRows);table(lastRows)}
|
||||||
function toggleTrend(key,id){trendVisible[key]=!trendVisible[key];if(!trendVisible.spend&&!trendVisible.conversions)trendVisible[key]=true;$(id).classList.toggle('off',!trendVisible[key]);$(id).setAttribute('aria-pressed',trendVisible[key]);trend(lastRows)}
|
function toggleTrend(key,id){trendVisible[key]=!trendVisible[key];if(!trendVisible.spend&&!trendVisible.conversions)trendVisible[key]=true;$(id).classList.toggle('off',!trendVisible[key]);$(id).setAttribute('aria-pressed',trendVisible[key]);trend(lastRows)}
|
||||||
$('toggleSpend').addEventListener('click',()=>toggleTrend('spend','toggleSpend'));$('toggleConversions').addEventListener('click',()=>toggleTrend('conversions','toggleConversions'));
|
$('toggleSpend').addEventListener('click',()=>toggleTrend('spend','toggleSpend'));$('toggleConversions').addEventListener('click',()=>toggleTrend('conversions','toggleConversions'));
|
||||||
fetch('data.json',{cache:'no-store'}).then(r=>r.ok?r.json():Promise.reject()).then(d=>{all=d.rows||[];singleOptions('pkg',all.filter(r=>r.status==='Active').map(r=>r.pkg));if([...$('pkg').options].some(o=>o.value==='com.amazon.mShop.android.shopping'))$('pkg').value='com.amazon.mShop.android.shopping';updateAnchoredFilters(true);$('fresh').innerHTML=`数据截至 <strong>${d.latestDate||'—'}</strong><br>更新于 ${new Date(d.generatedAt).toLocaleString('zh-CN',{hour12:false})}`;$('pkg').addEventListener('change',()=>{updateAnchoredFilters(true);render()});$('month').addEventListener('change',()=>{updateDateBounds(true);render()});for(const id of ['geo','source','start','end','group'])$(id).addEventListener('change',render);$('search').addEventListener('input',render);render()}).catch(()=>{$('fresh').textContent='数据加载失败';$('body').innerHTML='<tr><td colspan="13" class="empty">无法读取 data.json</td></tr>'});
|
Promise.all([fetch('data.json',{cache:'no-store'}),fetch('campaign_config.json',{cache:'no-store'})]).then(async([dataResponse,configResponse])=>{if(!dataResponse.ok||!configResponse.ok)throw new Error('source load failed');return [await dataResponse.json(),await configResponse.json()]}).then(([d,config])=>{campaignConfig=config||[];all=(d.rows||[]).map(r=>({...r,kpi:latestKpi(r)}));singleOptions('pkg',all.filter(r=>r.status==='Active').map(r=>r.pkg));if([...$('pkg').options].some(o=>o.value==='com.amazon.mShop.android.shopping'))$('pkg').value='com.amazon.mShop.android.shopping';updateAnchoredFilters(true);$('fresh').innerHTML=`数据截至 <strong>${d.latestDate||'—'}</strong><br>更新于 ${new Date(d.generatedAt).toLocaleString('zh-CN',{hour12:false})}`;$('pkg').addEventListener('change',()=>{updateAnchoredFilters(true);render()});$('month').addEventListener('change',()=>{updateDateBounds(true);render()});for(const id of ['geo','source','start','end','group'])$(id).addEventListener('change',render);$('search').addEventListener('input',render);render()}).catch(()=>{$('fresh').textContent='数据加载失败';$('body').innerHTML='<tr><td colspan="13" class="empty">无法读取 data.json 或 campaign_config.json</td></tr>'});
|
||||||
</script></body></html>
|
</script></body></html>
|
||||||
|
|||||||
Reference in New Issue
Block a user