feat: columna ad_daily.link_clicks (Meta clics enlace reales)
Persistimos link_clicks en warehouse (con backfill desde __lc), sync escribe la columna y creatividades la leen sin romper si falta. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -76,23 +76,39 @@ export async function GET(req: NextRequest) {
|
||||
|
||||
const agency = isAgencyRole(userRole(user));
|
||||
let daily: AdDailyRow[] = [];
|
||||
const selectWithLc =
|
||||
'date,platform,campaign_name,ad_id,ad_name,impressions,clicks,link_clicks,spend,platform_leads';
|
||||
const selectNoLc =
|
||||
'date,platform,campaign_name,ad_id,ad_name,impressions,clicks,spend,platform_leads';
|
||||
if (agency && ctx.viaView) {
|
||||
const tid = encodeURIComponent(ctx.tenantId);
|
||||
const d = await restGet<AdDailyRow[]>(
|
||||
`/rest/v1/ad_daily?tenant_id=eq.${tid}&date=gte.${since}&date=lte.${until}&select=date,platform,campaign_name,ad_id,ad_name,impressions,clicks,spend,platform_leads&order=date.desc&limit=5000`
|
||||
let d = await restGet<AdDailyRow[]>(
|
||||
`/rest/v1/ad_daily?tenant_id=eq.${tid}&date=gte.${since}&date=lte.${until}&select=${selectWithLc}&order=date.desc&limit=5000`
|
||||
);
|
||||
if (!d.ok && /link_clicks/i.test(String(d.error || ''))) {
|
||||
d = await restGet<AdDailyRow[]>(
|
||||
`/rest/v1/ad_daily?tenant_id=eq.${tid}&date=gte.${since}&date=lte.${until}&select=${selectNoLc}&order=date.desc&limit=5000`
|
||||
);
|
||||
}
|
||||
daily = d.data || [];
|
||||
} else {
|
||||
const { data } = await supabase
|
||||
let q = await supabase
|
||||
.from('ad_daily')
|
||||
.select(
|
||||
'date,platform,campaign_name,ad_id,ad_name,impressions,clicks,spend,platform_leads'
|
||||
)
|
||||
.select(selectWithLc)
|
||||
.gte('date', since)
|
||||
.lte('date', until)
|
||||
.order('date', { ascending: false })
|
||||
.limit(5000);
|
||||
daily = (data as AdDailyRow[]) || [];
|
||||
if (q.error && /link_clicks/i.test(q.error.message || '')) {
|
||||
q = await supabase
|
||||
.from('ad_daily')
|
||||
.select(selectNoLc)
|
||||
.gte('date', since)
|
||||
.lte('date', until)
|
||||
.order('date', { ascending: false })
|
||||
.limit(5000);
|
||||
}
|
||||
daily = (q.data as AdDailyRow[]) || [];
|
||||
}
|
||||
|
||||
const warnings: string[] = [];
|
||||
|
||||
@@ -365,27 +365,47 @@ async function replaceDaily(
|
||||
);
|
||||
}
|
||||
|
||||
// chunk inserts (link_clicks → ad_name marker until column exists)
|
||||
// chunk inserts — prefer link_clicks column; fallback to Meta `__lc:N` marker
|
||||
for (let i = 0; i < rows.length; i += 400) {
|
||||
const chunk = rows.slice(i, i + 400).map((r) => {
|
||||
const link = Math.round(r.link_clicks ?? r.clicks);
|
||||
const { link_clicks: _lc, ...rest } = r;
|
||||
if (r.platform === 'meta') {
|
||||
return { ...rest, ad_name: `__lc:${link}` };
|
||||
}
|
||||
return rest;
|
||||
});
|
||||
const r = await fetch(`${base}/rest/v1/ad_daily`, {
|
||||
const full = rows.slice(i, i + 400).map((r) => ({
|
||||
...r,
|
||||
link_clicks: Math.round(r.link_clicks ?? r.clicks),
|
||||
ad_name: r.ad_name || '',
|
||||
}));
|
||||
let r = await fetch(`${base}/rest/v1/ad_daily`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
Prefer: 'return=minimal',
|
||||
},
|
||||
body: JSON.stringify(chunk),
|
||||
body: JSON.stringify(full),
|
||||
});
|
||||
if (!r.ok) {
|
||||
throw new Error(`insert ad_daily: ${(await r.text()).slice(0, 220)}`);
|
||||
const errText = await r.text();
|
||||
if (!/link_clicks/i.test(errText)) {
|
||||
throw new Error(`insert ad_daily: ${errText.slice(0, 220)}`);
|
||||
}
|
||||
const legacy = full.map((row) => {
|
||||
const link = Math.round(row.link_clicks ?? row.clicks);
|
||||
const { link_clicks: _lc, ...rest } = row;
|
||||
if (row.platform === 'meta') {
|
||||
return { ...rest, ad_name: `__lc:${link}` };
|
||||
}
|
||||
return rest;
|
||||
});
|
||||
r = await fetch(`${base}/rest/v1/ad_daily`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
Prefer: 'return=minimal',
|
||||
},
|
||||
body: JSON.stringify(legacy),
|
||||
});
|
||||
if (!r.ok) {
|
||||
throw new Error(`insert ad_daily: ${(await r.text()).slice(0, 220)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,13 @@ export type AdDailyRow = {
|
||||
tenant_id?: string;
|
||||
};
|
||||
|
||||
/** Meta link clicks stored as ad_name `__lc:N` until link_clicks column is used. */
|
||||
/** Meta link clicks: column first, else `__lc:N` in ad_name (legacy), else all clicks. */
|
||||
export function linkClicksOf(r: AdDailyRow): number {
|
||||
const m = String(r.ad_name || '').match(/^__lc:(\d+(?:\.\d+)?)$/);
|
||||
if (m) return Number(m[1]) || 0;
|
||||
if (r.link_clicks != null && Number.isFinite(Number(r.link_clicks))) {
|
||||
return Number(r.link_clicks) || 0;
|
||||
}
|
||||
const m = String(r.ad_name || '').match(/^__lc:(\d+(?:\.\d+)?)$/);
|
||||
if (m) return Number(m[1]) || 0;
|
||||
return Number(r.clicks || 0) || 0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user