fix: Ads Meta conv/CPL = Clientes potenciales (sin sumar acciones solapadas)

Las creatividades inflaban ~3× al sumar lead + lead_grouped + registration.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-28 18:32:51 +02:00
parent 990915a051
commit 6d8dfaa79c
4 changed files with 36 additions and 30 deletions

View File

@@ -123,12 +123,20 @@ function aggregate(rows: AdDailyRow[]): PlatformKpis {
const impressions = active.reduce((a, r) => a + Number(r.impressions || 0), 0); const impressions = active.reduce((a, r) => a + Number(r.impressions || 0), 0);
const clicks = active.reduce((a, r) => a + Number(r.clicks || 0), 0); const clicks = active.reduce((a, r) => a + Number(r.clicks || 0), 0);
const conversions = active.reduce((a, r) => a + convOf(r), 0); const conversions = active.reduce((a, r) => a + convOf(r), 0);
// Round once after sum (Ads Manager style; Google stores fractional daily).
const conversionsRounded = Math.round(conversions * 100) / 100;
const ads = new Set( const ads = new Set(
active active
.map((r) => String(r.ad_id || campaignName(r) || '').trim()) .map((r) => String(r.ad_id || campaignName(r) || '').trim())
.filter((n) => n && n !== 'Sin nombre') .filter((n) => n && n !== 'Sin nombre')
).size; ).size;
return rates({ spend, impressions, clicks, conversions, ads }); return rates({
spend,
impressions,
clicks,
conversions: Math.round(conversionsRounded),
ads,
});
} }
function byCampaign(rows: AdDailyRow[]): CampaignRow[] { function byCampaign(rows: AdDailyRow[]): CampaignRow[] {
@@ -161,6 +169,7 @@ function byCampaign(rows: AdDailyRow[]): CampaignRow[] {
return [...map.values()] return [...map.values()]
.map((c) => ({ .map((c) => ({
...c, ...c,
conversions: Math.round(c.conversions * 100) / 100,
cpl: c.conversions > 0 ? c.spend / c.conversions : null, cpl: c.conversions > 0 ? c.spend / c.conversions : null,
})) }))
.sort((a, b) => b.spend - a.spend); .sort((a, b) => b.spend - a.spend);

View File

@@ -2,6 +2,7 @@
import { resolveMetaAccessToken } from '@/lib/ads-discovery'; import { resolveMetaAccessToken } from '@/lib/ads-discovery';
import { serviceHeaders, supabaseUrl } from '@/lib/agency'; import { serviceHeaders, supabaseUrl } from '@/lib/agency';
import { leadFromMetaActions } from '@/lib/meta-leads';
const META_GRAPH = 'https://graph.facebook.com/v21.0'; const META_GRAPH = 'https://graph.facebook.com/v21.0';
@@ -434,18 +435,9 @@ async function fetchMetaAdLevelInsights(
for (const row of raw.data || []) { for (const row of raw.data || []) {
const adId = str(row.ad_id); const adId = str(row.ad_id);
if (!adId) continue; if (!adId) continue;
let conversions = 0; // Same metric as ad_daily / Ads Manager «Clientes potenciales»
for (const a of row.actions || []) { // (do not sum lead + lead_grouped + registration — they overlap ~3×).
const t = (a.action_type || '').toLowerCase(); const conversions = leadFromMetaActions(row.actions);
if (
t === 'offsite_conversion.fb_pixel_complete_registration' ||
t === 'lead' ||
t === 'onsite_conversion.lead_grouped' ||
t.includes('complete_registration')
) {
conversions += n(a.value);
}
}
out.push({ out.push({
adId, adId,
adName: str(row.ad_name), adName: str(row.ad_name),

View File

@@ -2,6 +2,7 @@
import { serviceHeaders, supabaseUrl } from '@/lib/agency'; import { serviceHeaders, supabaseUrl } from '@/lib/agency';
import { resolveMetaAccessToken } from '@/lib/ads-discovery'; import { resolveMetaAccessToken } from '@/lib/ads-discovery';
import { leadFromMetaActions, nMeta } from '@/lib/meta-leads';
const META_GRAPH = 'https://graph.facebook.com/v21.0'; const META_GRAPH = 'https://graph.facebook.com/v21.0';
const GADS_API = 'https://googleads.googleapis.com/v22'; const GADS_API = 'https://googleads.googleapis.com/v22';
@@ -55,10 +56,7 @@ type AdDailyInsert = {
platform_leads: number; platform_leads: number;
}; };
function n(v: unknown): number { const n = nMeta;
const x = Number(v);
return Number.isFinite(x) ? x : 0;
}
function isoWeekMonday(d: Date): string { function isoWeekMonday(d: Date): string {
const x = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())); const x = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
@@ -67,19 +65,6 @@ function isoWeekMonday(d: Date): string {
return x.toISOString().slice(0, 10); return x.toISOString().slice(0, 10);
} }
function leadFromMetaActions(
actions?: { action_type?: string; value?: string }[]
): number {
if (!actions?.length) return 0;
// Exact Ads Manager «Clientes potenciales» for Meta Leads CRM campaigns.
// Do not fall back to lead / lead_grouped / pixel — those inflate past the
// Ads Manager period total when mixed with registration actions.
const primary = actions.find(
(a) => a.action_type === 'offsite_complete_registration_add_meta_leads'
);
return primary ? n(primary.value) : 0;
}
async function googleAccessToken(): Promise<string> { async function googleAccessToken(): Promise<string> {
const refresh = const refresh =
process.env.GOOGLE_ADS_REFRESH_TOKEN?.trim() || process.env.GOOGLE_ADS_REFRESH_TOKEN?.trim() ||

20
src/lib/meta-leads.ts Normal file
View File

@@ -0,0 +1,20 @@
/** Meta Ads «Clientes potenciales» ↔ platform_leads (same metric as Ads Manager). */
export function nMeta(v: unknown): number {
const x = Number(v);
return Number.isFinite(x) ? x : 0;
}
/**
* Exact Ads Manager «Clientes potenciales» for Meta Leads CRM campaigns.
* Do not sum lead / lead_grouped / pixel — those overlap and inflate totals.
*/
export function leadFromMetaActions(
actions?: { action_type?: string; value?: string }[]
): number {
if (!actions?.length) return 0;
const primary = actions.find(
(a) => a.action_type === 'offsite_complete_registration_add_meta_leads'
);
return primary ? nMeta(primary.value) : 0;
}