Add KPI event and CAC data pipeline

This commit is contained in:
Codex
2026-09-10 18:35:47 +08:00
parent 9796a896d2
commit bd806a834b
5 changed files with 258 additions and 68 deletions
+135 -63
View File
@@ -31,7 +31,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "idsp - 无事件要求",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -43,7 +43,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "idsp - 无事件要求",
"kpi": "-",
"remark": "与10599157 under在同一个客户下",
"channel": "mintegral"
},
@@ -55,7 +55,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "idsp - 无事件要求",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -91,7 +91,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Pause",
"kpi": "",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -103,7 +103,7 @@
"geo": "TH",
"platform": "MTG",
"status": "Active",
"kpi": "af_level_achieved, af_purchase",
"kpi": "af_purchase",
"remark": "0812 新上",
"channel": "mintegral"
},
@@ -115,7 +115,7 @@
"geo": "TH",
"platform": "MTG",
"status": "Active",
"kpi": "af_level_achieved, af_purchase",
"kpi": "af_purchase",
"remark": "",
"channel": "mintegral"
},
@@ -127,7 +127,7 @@
"geo": "TH",
"platform": "MTG",
"status": "Active",
"kpi": "af_level_achieved, af_purchase",
"kpi": "af_purchase",
"remark": "",
"channel": "mintegral"
},
@@ -139,7 +139,7 @@
"geo": "IN",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -150,8 +150,8 @@
"owner": "黄政",
"geo": "IN",
"platform": "MTG",
"status": "Active",
"kpi": "",
"status": "Pause",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -163,7 +163,7 @@
"geo": "ID",
"platform": "MTG",
"status": "Pause",
"kpi": "idsp - 无事件要求",
"kpi": "-",
"remark": "对照组和实验组用的同一个adping,人群包campaign没有新开adpingid给武汉",
"channel": "mintegral"
},
@@ -175,7 +175,7 @@
"geo": "PH",
"platform": "MTG",
"status": "Pause",
"kpi": "idsp - 无事件要求",
"kpi": "-",
"remark": "对照组和实验组用的同一个adping,人群包campaign没有新开adpingid给武汉",
"channel": "mintegral"
},
@@ -211,7 +211,7 @@
"geo": "PH",
"platform": "MTG",
"status": "Pause",
"kpi": "",
"kpi": "-",
"remark": "只有实验组",
"channel": "mintegral"
},
@@ -223,7 +223,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Pause",
"kpi": "",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -235,7 +235,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Pause",
"kpi": "",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -247,7 +247,7 @@
"geo": "ID",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -259,7 +259,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -271,7 +271,7 @@
"geo": "PH",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "8/14 还没上",
"channel": "mintegral"
},
@@ -283,7 +283,7 @@
"geo": "TH",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "8/14 还没上",
"channel": "mintegral"
},
@@ -295,7 +295,7 @@
"geo": "BR",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "8/14 还没上",
"channel": "mintegral"
},
@@ -307,7 +307,7 @@
"geo": "MY",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "8/14 还没上",
"channel": "mintegral"
},
@@ -319,7 +319,7 @@
"geo": "ID",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -331,7 +331,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -343,7 +343,7 @@
"geo": "PH",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -355,7 +355,7 @@
"geo": "TH",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -367,7 +367,7 @@
"geo": "BR",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -379,7 +379,7 @@
"geo": "MY",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -414,8 +414,8 @@
"owner": "周梓君",
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "",
"status": "Pause",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -427,7 +427,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "new_buyer>3%",
"kpi": "new_buyer",
"remark": "",
"channel": "mintegral"
},
@@ -439,7 +439,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "new_buyer>3%",
"kpi": "new_buyer",
"remark": "",
"channel": "mintegral"
},
@@ -451,7 +451,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "new_buyer>3%",
"kpi": "new_buyer",
"remark": "",
"channel": "mintegral"
},
@@ -463,7 +463,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "new_buyer>3%",
"kpi": "new_buyer",
"remark": "",
"channel": "mintegral"
},
@@ -475,7 +475,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "idsp - 无事件要求",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -487,7 +487,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "idsp - 无事件要求",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -619,7 +619,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "game.levelup",
"remark": "0826新上",
"channel": "mintegral"
},
@@ -631,7 +631,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "game.levelup",
"remark": "0826新上",
"channel": "mintegral"
},
@@ -643,7 +643,7 @@
"geo": "BR",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "af_purchase",
"remark": "0826新上",
"channel": "mintegral"
},
@@ -655,7 +655,7 @@
"geo": "BR",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "af_purchase",
"remark": "0826新上",
"channel": "mintegral"
},
@@ -667,7 +667,7 @@
"geo": "TH",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "af_purchase",
"remark": "0826新上",
"channel": "mintegral"
},
@@ -679,7 +679,7 @@
"geo": "TH",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "af_purchase",
"remark": "0826新上",
"channel": "mintegral"
},
@@ -691,7 +691,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "CPI订单,Install=New devicesKPI:new users>50%, cvr>0.5%(soft); 可用log in,play tracks, purchase事件优化质量",
"kpi": "login",
"remark": "0826新上",
"channel": "mintegral"
},
@@ -703,7 +703,7 @@
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "CPI订单,Install=New devicesKPI:new users>50%, cvr>0.5%(soft); 可用log in,play tracks, purchase事件优化质量",
"kpi": "login",
"remark": "0826新上",
"channel": "mintegral"
},
@@ -715,7 +715,7 @@
"geo": "AE",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "event1",
"remark": "",
"channel": "mintegral"
},
@@ -727,7 +727,7 @@
"geo": "ZA",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "event1",
"remark": "",
"channel": "mintegral"
},
@@ -739,7 +739,7 @@
"geo": "BR",
"platform": "BIGO",
"status": "Active",
"kpi": "",
"kpi": "event1",
"remark": "",
"channel": "bigo"
},
@@ -751,7 +751,7 @@
"geo": "AE",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "event1",
"remark": "",
"channel": "mintegral"
},
@@ -763,7 +763,7 @@
"geo": "ZA",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "event1",
"remark": "",
"channel": "mintegral"
},
@@ -775,7 +775,7 @@
"geo": "BR",
"platform": "BIGO",
"status": "Active",
"kpi": "",
"kpi": "event1",
"remark": "",
"channel": "bigo"
},
@@ -783,11 +783,11 @@
"pkg": "com.esprit.rpg.lttlefirykng",
"adping_id": "11655596",
"group": "对照组",
"owner": "",
"owner": "万科",
"geo": "RU",
"platform": "BIGO",
"status": "Active",
"kpi": "",
"kpi": "game.levelup",
"remark": "0827新上",
"channel": "bigo"
},
@@ -795,11 +795,11 @@
"pkg": "com.esprit.rpg.lttlefirykng",
"adping_id": "11763482",
"group": "实验组",
"owner": "",
"owner": "万科",
"geo": "RU",
"platform": "BIGO",
"status": "Active",
"kpi": "",
"kpi": "game.levelup",
"remark": "0827新上",
"channel": "bigo"
},
@@ -811,7 +811,7 @@
"geo": "MY",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "af_purchase",
"remark": "0828新上",
"channel": "mintegral"
},
@@ -823,7 +823,7 @@
"geo": "MY",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "af_purchase",
"remark": "0828新上",
"channel": "mintegral"
},
@@ -834,8 +834,8 @@
"owner": "",
"geo": "ID",
"platform": "MTG",
"status": "Active",
"kpi": "",
"status": "Pause",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -847,7 +847,7 @@
"geo": "ID",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "",
"channel": "mintegral"
},
@@ -859,7 +859,7 @@
"geo": "ID",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "0831新上",
"channel": "mintegral"
},
@@ -871,7 +871,7 @@
"geo": "ID",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "0831新上",
"channel": "mintegral"
},
@@ -883,7 +883,7 @@
"geo": "IN",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "0901新上",
"channel": "mintegral"
},
@@ -895,8 +895,80 @@
"geo": "IN",
"platform": "MTG",
"status": "Active",
"kpi": "",
"kpi": "-",
"remark": "0901新上",
"channel": "mintegral"
},
{
"pkg": "sports.caliente.mx.calientedeportes",
"adping_id": "5365722",
"group": "对照组",
"owner": "詹涵粤",
"geo": "MX",
"platform": "MTG",
"status": "Active",
"kpi": "singup",
"remark": "",
"channel": "mintegral"
},
{
"pkg": "sports.caliente.mx.calientedeportes",
"adping_id": "11924654",
"group": "实验组",
"owner": "詹涵粤",
"geo": "MX",
"platform": "MTG",
"status": "Active",
"kpi": "singup",
"remark": "",
"channel": "mintegral"
},
{
"pkg": "astrum.ent.m2alyona",
"adping_id": "12038500",
"group": "实验组",
"owner": "谭超",
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "af_purchase",
"remark": "",
"channel": "mintegral"
},
{
"pkg": "astrum.ent.m2alyona",
"adping_id": "11690181",
"group": "对照组",
"owner": "谭超",
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "af_purchase",
"remark": "",
"channel": "mintegral"
},
{
"pkg": "asrtum.ent.domovyata",
"adping_id": "12038344",
"group": "实验组",
"owner": "黄政",
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "af_purchase",
"remark": "",
"channel": "mintegral"
},
{
"pkg": "asrtum.ent.domovyata",
"adping_id": "7872826",
"group": "对照组",
"owner": "黄政",
"geo": "RU",
"platform": "MTG",
"status": "Active",
"kpi": "af_purchase",
"remark": "",
"channel": "mintegral"
}
]
]
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -52,11 +52,11 @@ 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 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!=='-'};
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,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.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,spend:0};const vals=[['Spend',money(t.spend)],['Event',e.eventKnown?n(e.event):'—'],['CAC',e.eventKnown&&e.event?money(e.spend/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">${x[0]}</div><div class="value">${x[1]}</div></div>`).join('')}
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};const vals=[['Spend',money(t.spend)],['Event',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">${x[0]}</div><div class="value">${x[1]}</div></div>`).join('')}
function compact(v,prefix=''){return prefix+Intl.NumberFormat('en-US',{notation:'compact',maximumFractionDigits:1}).format(v)}
function trend(rows){const d=aggregate(rows,['date']).sort((a,b)=>a.date.localeCompare(b.date)),svg=$('trend'),tip=$('trendTip');if(!d.length){svg.innerHTML='<text x="450" y="135" text-anchor="middle" class="axis-label">当前筛选没有数据</text>';return}const W=900,H=270,L=62,R=66,T=20,B=38,maxS=Math.max(...d.map(x=>x.spend),1),maxC=Math.max(...d.map(x=>x.conversions),1),x=i=>L+(W-L-R)*(d.length===1?.5:i/(d.length-1)),y=(v,max)=>T+(H-T-B)*(1-v/max),path=(key,max)=>d.map((r,i)=>(i?'L':'M')+x(i).toFixed(1)+' '+y(r[key],max).toFixed(1)).join(' ');let parts=[];for(let i=0;i<=4;i++){const yy=T+(H-T-B)*i/4,ratio=1-i/4;parts.push(`<line class="gridline" x1="${L}" y1="${yy}" x2="${W-R}" y2="${yy}"/><text class="axis-label" x="${L-8}" y="${yy+4}" text-anchor="end">${compact(maxS*ratio,'$')}</text><text class="axis-label" x="${W-R+8}" y="${yy+4}">${compact(maxC*ratio)}</text>`)}const labelStep=Math.max(1,Math.ceil(d.length/8));parts.push(...d.map((r,i)=>(i%labelStep===0||i===d.length-1)?`<text class="axis-label" x="${x(i)}" y="${H-12}" text-anchor="middle">${r.date.slice(5)}</text>`:''));parts.push(`<text class="axis-title" x="${L}" y="11" fill="#1769e0">Spend (USD)</text><text class="axis-title" x="${W-R}" y="11" text-anchor="end" fill="#16825d">Conversions</text>`);if(trendVisible.spend)parts.push(`<path class="spend-line" d="${path('spend',maxS)}"/>`);if(trendVisible.conversions)parts.push(`<path class="conv-line" d="${path('conversions',maxC)}"/>`);parts.push(...d.map((r,i)=>`<g class="trend-hit" data-index="${i}"><circle cx="${x(i)}" cy="${trendVisible.spend?y(r.spend,maxS):y(r.conversions,maxC)}" r="12" fill="transparent"/><circle class="point" cx="${x(i)}" cy="${trendVisible.spend?y(r.spend,maxS):y(r.conversions,maxC)}" r="3.5" fill="${trendVisible.spend?'#1769e0':'#16825d'}"/></g>`));svg.innerHTML=parts.join('');svg.querySelectorAll('.trend-hit').forEach(el=>{el.addEventListener('mouseenter',()=>{const r=d[+el.dataset.index];tip.innerHTML=`<strong>${r.date}</strong><br>Spend:${money(r.spend)}<br>Conversions:${n(r.conversions)}`;tip.style.display='block'});el.addEventListener('mousemove',e=>{const box=svg.getBoundingClientRect(),panel=svg.parentElement.getBoundingClientRect();tip.style.left=Math.min(e.clientX-panel.left+12,panel.width-tip.offsetWidth-12)+'px';tip.style.top=(e.clientY-panel.top-tip.offsetHeight-10)+'px'});el.addEventListener('mouseleave',()=>tip.style.display='none')})}
function table(rows){const d=aggregate(rows,['date','group','campaign','kpi']).sort((a,b)=>b.date.localeCompare(a.date)||(a.group==='对照组'?-1:1)-(b.group==='对照组'?-1:1)||b.spend-a.spend);$('count').textContent=`${n(d.length)} 行`;const limited=d.slice(0,2000),dateCounts={};for(const r of limited)dateCounts[r.date]=(dateCounts[r.date]||0)+1;const seen=new Set;$('body').innerHTML=limited.map(r=>{const first=!seen.has(r.date),required=tracksEvent(r);seen.add(r.date);return`<tr class="${first?'date-start':''}">${first?`<td class="date-cell" rowspan="${dateCounts[r.date]}">${r.date}</td>`:''}<td class="${r.group==='对照组'?'control':'experiment'}">${r.group}</td><td>${r.campaign}</td><td>${r.kpi||'-'}</td><td class="num">${required&&r.eventKnown?n(r.event):'—'}</td><td class="num">${money(r.spend)}</td><td class="num">${required&&r.eventKnown&&r.event?money(r.spend/r.event):'—'}</td><td class="num">${n(r.impression)}</td><td class="num">${n(r.click)}</td><td class="num">${n(r.conversions)}</td><td class="num">${pct(rate(r.click,r.impression))}</td><td class="num">${pct(rate(r.conversions,r.click))}</td><td class="num">${r.conversions?money(r.spend/r.conversions):'—'}</td></tr>`}).join('')||'<tr><td colspan="13" class="empty">当前筛选没有数据</td></tr>'}
function table(rows){const d=aggregate(rows,['date','group','campaign','kpi']).sort((a,b)=>b.date.localeCompare(a.date)||(a.group==='对照组'?-1:1)-(b.group==='对照组'?-1:1)||b.spend-a.spend);$('count').textContent=`${n(d.length)} 行`;const limited=d.slice(0,2000),dateCounts={};for(const r of limited)dateCounts[r.date]=(dateCounts[r.date]||0)+1;const seen=new Set;$('body').innerHTML=limited.map(r=>{const first=!seen.has(r.date),required=tracksEvent(r);seen.add(r.date);return`<tr class="${first?'date-start':''}">${first?`<td class="date-cell" rowspan="${dateCounts[r.date]}">${r.date}</td>`:''}<td class="${r.group==='对照组'?'control':'experiment'}">${r.group}</td><td>${r.campaign}</td><td>${r.kpi||'-'}</td><td class="num">${required&&r.eventKnown?n(r.event):'—'}</td><td class="num">${money(r.spend)}</td><td class="num">${required&&r.eventKnown&&r.event?money(r.eventSpend/r.event):'—'}</td><td class="num">${n(r.impression)}</td><td class="num">${n(r.click)}</td><td class="num">${n(r.conversions)}</td><td class="num">${pct(rate(r.click,r.impression))}</td><td class="num">${pct(rate(r.conversions,r.click))}</td><td class="num">${r.conversions?money(r.spend/r.conversions):'—'}</td></tr>`}).join('')||'<tr><td colspan="13" class="empty">当前筛选没有数据</td></tr>'}
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)}
$('toggleSpend').addEventListener('click',()=>toggleTrend('spend','toggleSpend'));$('toggleConversions').addEventListener('click',()=>toggleTrend('conversions','toggleConversions'));
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env python3
import csv
import json
import os
import re
import sys
import tempfile
import urllib.request
import zipfile
from pathlib import Path
from tableauhyperapi import Connection, HyperProcess, Telemetry
SERVER = os.environ.get("TABLEAU_SERVER", "https://mars.ushareit.org").rstrip("/")
API_VERSION = os.environ.get("TABLEAU_API_VERSION", "3.15")
SITE_CONTENT_URL = os.environ.get("TABLEAU_SITE_CONTENT_URL", "")
WORKBOOK_ID = os.environ.get("TABLEAU_EVENT_WORKBOOK_ID", "ad567777-aa52-4c2f-89c3-5acfed4fdbba")
def request_json(url, *, body=None, headers=None):
request = urllib.request.Request(url, data=body, headers=headers or {})
with urllib.request.urlopen(request, timeout=120) as response:
return json.load(response)
def main():
if len(sys.argv) != 2:
raise SystemExit("usage: refresh_events.py OUTPUT.csv")
output_path = sys.argv[1]
pat_name = os.environ.get("TABLEAU_PAT_NAME", "")
pat_secret = os.environ.get("TABLEAU_PAT_SECRET", "")
if not pat_name or not pat_secret:
raise SystemExit("TABLEAU_PAT_NAME and TABLEAU_PAT_SECRET are required")
signin_body = json.dumps({"credentials": {
"personalAccessTokenName": pat_name,
"personalAccessTokenSecret": pat_secret,
"site": {"contentUrl": SITE_CONTENT_URL},
}}).encode()
signin = request_json(
f"{SERVER}/api/{API_VERSION}/auth/signin",
body=signin_body,
headers={"Content-Type": "application/json", "Accept": "application/json"},
)["credentials"]
auth_token = signin["token"]
site_id = signin["site"]["id"]
try:
with tempfile.TemporaryDirectory(prefix="manta-events-") as temp_dir:
archive_path = Path(temp_dir) / "workbook.twbx"
download = urllib.request.Request(
f"{SERVER}/api/{API_VERSION}/sites/{site_id}/workbooks/{WORKBOOK_ID}/content",
headers={"X-Tableau-Auth": auth_token},
)
with urllib.request.urlopen(download, timeout=180) as response:
archive_path.write_bytes(response.read())
with zipfile.ZipFile(archive_path) as archive:
members = [name for name in archive.namelist() if name.endswith(".hyper")]
if len(members) != 1:
raise SystemExit(f"expected one Hyper extract, found {len(members)}")
archive.extract(members[0], temp_dir)
hyper_path = Path(temp_dir) / members[0]
query = '''
SELECT "dt", "pkg_name", "adping_id", "offer_name", "event_name",
SUM("event_num")
FROM "Extract"."Extract"
GROUP BY "dt", "pkg_name", "adping_id", "offer_name", "event_name"
ORDER BY "dt", "pkg_name", "offer_name", "event_name"
'''
with HyperProcess(Telemetry.DO_NOT_SEND_USAGE_DATA_TO_TABLEAU) as process:
with Connection(process.endpoint, str(hyper_path)) as connection:
rows = connection.execute_list_query(query)
with open(output_path, "w", encoding="utf-8", newline="") as handle:
writer = csv.writer(handle)
writer.writerow(("dt", "pkg_name", "adping_id", "offer_name", "event_name", "event_num"))
writer.writerows(rows)
dates = sorted({str(row[0]) for row in rows})
print(json.dumps({"rows": len(rows), "dates": dates}))
finally:
try:
signout = urllib.request.Request(
f"{SERVER}/api/{API_VERSION}/auth/signout", data=b"",
headers={"X-Tableau-Auth": auth_token},
)
urllib.request.urlopen(signout, timeout=30).close()
except Exception:
pass
if __name__ == "__main__":
main()
+27 -1
View File
@@ -3,7 +3,10 @@ import csv, io, json, os, re, sys
from datetime import datetime
from zoneinfo import ZoneInfo
src, target = sys.argv[1:]
if len(sys.argv) not in (3, 4):
raise SystemExit('usage: transform_tableau.py SPEND.csv YYYY-MM-DD [EVENTS.csv]')
src, target = sys.argv[1:3]
events_path = sys.argv[3] if len(sys.argv) == 4 else None
raw = open(src, 'rb').read()
if b'<html' in raw[:1000].lower():
raise SystemExit('Tableau authentication redirected to HTML')
@@ -33,6 +36,22 @@ def num(v):
try: return float(str(v or '0').replace(',', '').replace('%', ''))
except ValueError: return 0
def event_names(value):
value = str(value or '').strip()
if not value or value == '-': return []
return [x.strip().lower() for x in re.split(r'[,,;/;\n]+', value) if x.strip()]
events, covered_dates = {}, set()
if events_path:
with open(events_path, encoding='utf-8-sig', newline='') as f:
for event_row in csv.DictReader(f):
dt = re.sub(r'[^0-9]', '', event_row.get('dt', ''))
if len(dt) == 8: dt = f'{dt[:4]}-{dt[4:6]}-{dt[6:]}'
covered_dates.add(dt)
key = (dt, event_row.get('pkg_name','').strip(), event_row.get('offer_name','').strip(),
event_row.get('event_name','').strip().lower())
events[key] = events.get(key, 0) + num(event_row.get('event_num'))
config = [r for r in json.load(open('campaign_config.json', encoding='utf-8')) if r['status'].lower() == 'active']
experiment = {}
for r in config:
@@ -66,8 +85,15 @@ for raw_row in matrix[1:] if has_header else matrix:
unresolved.append(campaign)
continue
group, shown, adping = '对照组', 'General campaign', ''
date = cell(raw_row, 'date').strip() or target
required = event_names(meta.get('kpi',''))
if not required or source != 'mintegral' or date not in covered_dates:
event = None
else:
event = sum(events.get((date, pkg, campaign, name), 0) for name in required)
rows.append({'date': cell(raw_row, 'date').strip() or target, 'pkg':pkg, 'geo':meta['geo'], 'source':source,
'group':group, 'adpingId':adping, 'campaign':shown, 'status':'Active', 'kpi':meta.get('kpi',''),
'event':event,
'impression':num(cell(raw_row,'impression')), 'click':num(cell(raw_row,'click')),
'conversions':num(cell(raw_row,'conversions')), 'spend':num(cell(raw_row,'spend'))})