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:
@@ -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);
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
20
src/lib/meta-leads.ts
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user