- Posicionamentos: novas campanhas saem só com Feed+Stories+Reels (FB+IG), sem Audience Network, Messenger, coluna direita ou Marketplace - Botão "Corrigir Posicionamentos" (ícone Target) em cada ad set existente — busca targeting atual, preserva geo/idade/interesses e aplica as restrições - Stats: compras, alcance, CPA e reach nos cards do dashboard e na tabela (campanha, ad set e anúncio); bug de leads/ROAS sempre 0 no nível campanha corrigido - Auditoria IA: prompt ciente do objetivo (Leads/Vendas/Tráfego), passa todas as métricas relevantes (CTR, CPC, CPL, CPA, alcance, impressões); hero da tela mostra cards dinâmicos conforme o tipo de campanha - strategist.ts: corrigido acesso indevido a actions/purchase_roas nos insights mapeados Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
216 lines
8.5 KiB
TypeScript
216 lines
8.5 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { createMetaMCPClient } from '@/lib/mcp/client';
|
|
import { getCampaignDetails } from '@/lib/mcp/tools';
|
|
import { getAccessToken } from '@/lib/auth/session';
|
|
import { getAIClient } from '@/lib/ai/client';
|
|
|
|
const ANALYSIS_MODEL = 'gpt-5.4-mini';
|
|
|
|
// Extrai o valor de uma action pelo tipo
|
|
function findAction(actions: any[], type: string): number {
|
|
return parseInt(actions.find((a: any) => a.action_type === type)?.value || '0', 10);
|
|
}
|
|
|
|
// Rótulo legível do objetivo da campanha
|
|
function objectiveLabel(objective: string): string {
|
|
const map: Record<string, string> = {
|
|
OUTCOME_LEADS: 'Geração de Leads',
|
|
OUTCOME_SALES: 'Vendas (Conversões)',
|
|
OUTCOME_TRAFFIC: 'Tráfego',
|
|
OUTCOME_AWARENESS: 'Reconhecimento de Marca',
|
|
OUTCOME_ENGAGEMENT: 'Engajamento',
|
|
OUTCOME_APP_PROMOTION: 'Promoção de App',
|
|
};
|
|
return map[objective] || objective;
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const token = await getAccessToken();
|
|
if (!token) {
|
|
return NextResponse.json({ success: false, error: 'Not authenticated' }, { status: 401 });
|
|
}
|
|
|
|
const { campaignId } = await request.json();
|
|
if (!campaignId) {
|
|
return NextResponse.json({ success: false, error: 'Campaign ID required' }, { status: 400 });
|
|
}
|
|
|
|
const client = await createMetaMCPClient(token);
|
|
let campaignData: any;
|
|
|
|
try {
|
|
const result = await getCampaignDetails(client, campaignId);
|
|
campaignData = Array.isArray(result) ? result[0] : result;
|
|
} finally {
|
|
await client.close();
|
|
}
|
|
|
|
if (!campaignData || !campaignData.id) {
|
|
return NextResponse.json({ success: false, error: 'Campaign not found on Meta' }, { status: 404 });
|
|
}
|
|
|
|
const insights = campaignData.insights?.data?.[0] || {};
|
|
const adsets = campaignData.adsets?.data || [];
|
|
const actions = insights.actions || [];
|
|
const objective = campaignData.objective || '';
|
|
|
|
const spend = parseFloat(insights.spend || '0');
|
|
const ctr = parseFloat(insights.ctr || '0');
|
|
const cpc = parseFloat(insights.cpc || '0');
|
|
const cpm = parseFloat(insights.cpm || '0');
|
|
const reach = parseInt(insights.reach || '0', 10);
|
|
const impressions = parseInt(insights.impressions || '0', 10);
|
|
const leads = findAction(actions, 'lead') || findAction(actions, 'onsite_conversion.lead_grouped');
|
|
const purchases = findAction(actions, 'omni_purchase');
|
|
const roasRaw = parseFloat(insights.purchase_roas?.[0]?.value || '0');
|
|
const cpl = leads > 0 ? spend / leads : 0;
|
|
const cpa = purchases > 0 ? spend / purchases : 0;
|
|
|
|
// Extract Ads & Creatives
|
|
const adsToAnalyze: any[] = [];
|
|
for (const adset of adsets) {
|
|
for (const ad of adset.ads?.data || []) {
|
|
const adIns = ad.insights?.data?.[0] || {};
|
|
const adActions = adIns.actions || [];
|
|
const creative = ad.creative || {};
|
|
|
|
let body = creative.body || '';
|
|
let title = creative.title || '';
|
|
let imageUrl = creative.image_url || creative.thumbnail_url || '';
|
|
|
|
if (creative.object_story_spec?.link_data) {
|
|
const ld = creative.object_story_spec.link_data;
|
|
body = body || ld.message || '';
|
|
title = title || ld.name || '';
|
|
}
|
|
|
|
if (creative.asset_feed_spec) {
|
|
const feed = creative.asset_feed_spec;
|
|
body = body || feed.bodies?.map((b: any) => b.text).join(' | ') || '';
|
|
title = title || feed.titles?.map((t: any) => t.text).join(' | ') || '';
|
|
if (!imageUrl && feed.images?.length > 0) {
|
|
imageUrl = feed.images[0].url || feed.images[0].thumbnail_url || '';
|
|
}
|
|
}
|
|
|
|
const adLeads = findAction(adActions, 'lead') || findAction(adActions, 'onsite_conversion.lead_grouped');
|
|
const adPurchases = findAction(adActions, 'omni_purchase');
|
|
const adSpend = parseFloat(adIns.spend || '0');
|
|
|
|
adsToAnalyze.push({
|
|
ad_name: ad.name,
|
|
spend: adSpend.toFixed(2),
|
|
ctr: parseFloat(adIns.ctr || '0').toFixed(2) + '%',
|
|
cpc: 'R$ ' + parseFloat(adIns.cpc || '0').toFixed(2),
|
|
leads: adLeads,
|
|
purchases: adPurchases,
|
|
cpl: adLeads > 0 ? 'R$ ' + (adSpend / adLeads).toFixed(2) : null,
|
|
cpa: adPurchases > 0 ? 'R$ ' + (adSpend / adPurchases).toFixed(2) : null,
|
|
roas: adIns.purchase_roas?.[0]?.value ? parseFloat(adIns.purchase_roas[0].value).toFixed(2) + 'x' : null,
|
|
body: body || '(sem texto)',
|
|
title: title || '(sem título)',
|
|
image_url: imageUrl,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Monta o contexto de métricas de forma objetiva para o tipo de campanha
|
|
const isLeadGen = objective === 'OUTCOME_LEADS' || (leads > 0 && purchases === 0);
|
|
const isSales = objective === 'OUTCOME_SALES' || purchases > 0;
|
|
|
|
const metricsBlock = [
|
|
`Objetivo: ${objectiveLabel(objective)}`,
|
|
`Gasto (30d): R$ ${spend.toFixed(2)}`,
|
|
`Alcance: ${reach.toLocaleString('pt-BR')} pessoas`,
|
|
`Impressões: ${impressions.toLocaleString('pt-BR')}`,
|
|
`CTR: ${ctr.toFixed(2)}%`,
|
|
`CPC: R$ ${cpc.toFixed(2)}`,
|
|
`CPM: R$ ${cpm.toFixed(2)}`,
|
|
leads > 0 ? `Leads gerados: ${leads} | CPL: R$ ${cpl.toFixed(2)}` : null,
|
|
purchases > 0 ? `Compras: ${purchases} | CPA: R$ ${cpa.toFixed(2)} | ROAS: ${roasRaw.toFixed(2)}x` : null,
|
|
].filter(Boolean).join('\n');
|
|
|
|
const focusInstructions = isLeadGen
|
|
? 'Esta é uma campanha de LEADS. Priorize: CPL (custo por lead), volume de leads, qualidade do copy para conversão em formulário.'
|
|
: isSales
|
|
? 'Esta é uma campanha de VENDAS. Priorize: ROAS, CPA (custo por compra), volume de compras e lucratividade.'
|
|
: 'Esta é uma campanha de TRÁFEGO. Priorize: CTR, CPC, qualidade dos cliques e relevância do público.';
|
|
|
|
const openai = getAIClient();
|
|
|
|
const prompt = `Você é um Especialista Sênior em Tráfego Pago e Copywriting com foco no mercado brasileiro.
|
|
Faça uma auditoria completa e honesta desta campanha no Meta Ads.
|
|
|
|
${focusInstructions}
|
|
|
|
DADOS DA CAMPANHA:
|
|
Nome: ${campaignData.name}
|
|
${metricsBlock}
|
|
|
|
ANÚNCIOS (CRIATIVOS E MÉTRICAS INDIVIDUAIS):
|
|
${JSON.stringify(adsToAnalyze, null, 2)}
|
|
|
|
Retorne SOMENTE um JSON válido, sem markdown, com esta estrutura exata:
|
|
{
|
|
"diagnostico_geral": "Parágrafo forte resumindo saúde da campanha, mencionando a métrica-chave do objetivo (CPL para leads, ROAS/CPA para vendas, CTR para tráfego).",
|
|
"analise_copy": ["Ponto sobre texto do anúncio X...", "Sugestão específica de melhoria..."],
|
|
"analise_imagem": ["Avaliação do visual...", "Sugestão de formato ou abordagem..."],
|
|
"analise_metricas": ["Métrica X está Y — o que significa...", "Comparativo com benchmark..."],
|
|
"acao_recomendada": ["Ação imediata 1...", "Ação 2..."],
|
|
"nova_estrategia_sugerida": {
|
|
"nicho_sugerido": "ecommerce / info_produto / negocios_locais / etc",
|
|
"produto_foco": "Produto ou oferta que deve ser o foco",
|
|
"publico_alvo": "Público ideal baseado nos dados",
|
|
"angulos_de_venda": ["Ângulo 1", "Ângulo 2"]
|
|
}
|
|
}`;
|
|
|
|
const completion = await openai.chat.completions.create({
|
|
model: ANALYSIS_MODEL,
|
|
messages: [{ role: 'user', content: prompt }],
|
|
response_format: { type: 'json_object' },
|
|
});
|
|
|
|
const textRes = completion.choices[0]?.message?.content || '';
|
|
let jsonRes: any;
|
|
|
|
try {
|
|
const cleaned = textRes.replace(/^```json/m, '').replace(/```$/m, '').trim();
|
|
jsonRes = JSON.parse(cleaned);
|
|
} catch {
|
|
console.error('[Analysis] Failed to parse AI response:', textRes);
|
|
return NextResponse.json({ success: false, error: 'Failed to parse AI response' }, { status: 500 });
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
data: {
|
|
campaign_metrics: {
|
|
spend: spend.toFixed(2),
|
|
ctr: ctr.toFixed(2),
|
|
cpc: cpc.toFixed(2),
|
|
cpm: cpm.toFixed(2),
|
|
reach,
|
|
impressions,
|
|
leads,
|
|
purchases,
|
|
cpl: cpl > 0 ? cpl.toFixed(2) : null,
|
|
cpa: cpa > 0 ? cpa.toFixed(2) : null,
|
|
roas: roasRaw > 0 ? roasRaw.toFixed(2) : null,
|
|
objective,
|
|
objective_label: objectiveLabel(objective),
|
|
},
|
|
ads_count: adsToAnalyze.length,
|
|
ads: adsToAnalyze,
|
|
analysis: jsonRes,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('[Analysis] API Error:', error);
|
|
return NextResponse.json(
|
|
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|