Show every Google PMax text, image and YouTube asset in Ads zoom.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -183,7 +183,7 @@ export async function GET(req: NextRequest) {
|
|||||||
dailyRows: daily,
|
dailyRows: daily,
|
||||||
since,
|
since,
|
||||||
until,
|
until,
|
||||||
limit: 48,
|
limit: 80,
|
||||||
});
|
});
|
||||||
googleAds = google.ads.length
|
googleAds = google.ads.length
|
||||||
? google.ads
|
? google.ads
|
||||||
|
|||||||
@@ -17,26 +17,29 @@ export function proxiedMediaUrl(raw: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function adHeadlines(ad: AdCreativeCard): string[] {
|
function adHeadlines(ad: AdCreativeCard): string[] {
|
||||||
if (ad.headlines?.length) return ad.headlines.slice(0, 5);
|
const all = ad.headlines?.length
|
||||||
return (ad.headline || '')
|
? ad.headlines
|
||||||
.split('\n')
|
: (ad.headline || '')
|
||||||
.map((s) => s.trim())
|
.split('\n')
|
||||||
.filter(Boolean)
|
.map((s) => s.trim())
|
||||||
.slice(0, 5);
|
.filter(Boolean);
|
||||||
|
if (ad.platform === 'google') return all;
|
||||||
|
return all.slice(0, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
function adBodies(ad: AdCreativeCard): string[] {
|
function adBodies(ad: AdCreativeCard): string[] {
|
||||||
if (ad.bodies?.length) return ad.bodies.slice(0, 5);
|
const all = ad.bodies?.length
|
||||||
// Legacy: primaryText used to hold bodies
|
? ad.bodies
|
||||||
return (ad.primaryText || '')
|
: (ad.primaryText || '')
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean);
|
||||||
.slice(0, 5);
|
if (ad.platform === 'google') return all;
|
||||||
|
return all.slice(0, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
function adLinkDescriptions(ad: AdCreativeCard): string[] {
|
function adLinkDescriptions(ad: AdCreativeCard): string[] {
|
||||||
if (ad.descriptions?.length) return ad.descriptions.slice(0, 5);
|
if (ad.descriptions?.length) return ad.descriptions;
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,19 +161,19 @@ function previewFor(ad: AdCreativeCard): { url: string; aspect: CreativeAspect }
|
|||||||
function formatThumbs(ad: AdCreativeCard): { url: string; aspect: CreativeAspect; label: string }[] {
|
function formatThumbs(ad: AdCreativeCard): { url: string; aspect: CreativeAspect; label: string }[] {
|
||||||
if (ad.mediaType !== 'pmax' && ad.channel !== 'pmax') return [];
|
if (ad.mediaType !== 'pmax' && ad.channel !== 'pmax') return [];
|
||||||
const assets = adAssets(ad).filter(
|
const assets = adAssets(ad).filter(
|
||||||
(a) => (a.kind === 'image' || a.kind === 'logo' || a.kind === 'youtube') && a.url
|
(a) =>
|
||||||
|
(a.kind === 'image' || a.kind === 'logo' || a.kind === 'youtube') && a.url
|
||||||
);
|
);
|
||||||
const order: CreativeAspect[] = ['1:1', '4:5', '16:9', '9:16'];
|
|
||||||
const out: { url: string; aspect: CreativeAspect; label: string }[] = [];
|
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const asp of order) {
|
const out: { url: string; aspect: CreativeAspect; label: string }[] = [];
|
||||||
const hit = assets.find((a) => resolveAspect(a) === asp && !seen.has(a.url));
|
for (const a of assets) {
|
||||||
if (!hit) continue;
|
if (seen.has(a.url)) continue;
|
||||||
seen.add(hit.url);
|
seen.add(a.url);
|
||||||
|
const asp = resolveAspect(a);
|
||||||
out.push({
|
out.push({
|
||||||
url: proxiedMediaUrl(hit.url),
|
url: proxiedMediaUrl(a.url),
|
||||||
aspect: asp,
|
aspect: asp === 'unknown' ? '1:1' : asp,
|
||||||
label: asp,
|
label: a.label || asp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
@@ -451,15 +454,15 @@ function CreativeCard({
|
|||||||
<Maximize2 className="w-3.5 h-3.5" /> Zoom
|
<Maximize2 className="w-3.5 h-3.5" /> Zoom
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{pmaxFormats.length > 1 ? (
|
{pmaxFormats.length > 0 ? (
|
||||||
<div className="flex flex-wrap gap-1.5 justify-center">
|
<div className="flex flex-wrap gap-1.5 justify-center">
|
||||||
{pmaxFormats.map((f) => (
|
{pmaxFormats.slice(0, 12).map((f, i) => (
|
||||||
<button
|
<button
|
||||||
key={f.label}
|
key={`${f.url}-${i}`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onZoom}
|
onClick={onZoom}
|
||||||
className="w-12 overflow-hidden rounded-md border border-gray-200 bg-gray-50"
|
className="w-12 overflow-hidden rounded-md border border-gray-200 bg-gray-50"
|
||||||
title={`Formato ${f.label}`}
|
title={f.label}
|
||||||
>
|
>
|
||||||
<div className={`${aspectClass(f.aspect)} w-full`}>
|
<div className={`${aspectClass(f.aspect)} w-full`}>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
@@ -475,6 +478,11 @@ function CreativeCard({
|
|||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
{pmaxFormats.length > 12 ? (
|
||||||
|
<p className="w-full text-center text-[10px] text-gray-500">
|
||||||
|
+{pmaxFormats.length - 12} en Zoom
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -585,21 +593,24 @@ export function CreativeZoomModal({
|
|||||||
const images = assets.filter((a) => a.kind === 'image' || a.kind === 'logo');
|
const images = assets.filter((a) => a.kind === 'image' || a.kind === 'logo');
|
||||||
const videos = assets.filter((a) => a.kind === 'youtube' || a.kind === 'video');
|
const videos = assets.filter((a) => a.kind === 'youtube' || a.kind === 'video');
|
||||||
const { url: main, aspect: mainAspect } = previewFor(ad);
|
const { url: main, aspect: mainAspect } = previewFor(ad);
|
||||||
const previewAssetUrl = main.replace(/^\/api\/ad-tracker\/media\?url=/, '');
|
|
||||||
const otherImages = images.filter((a) => {
|
|
||||||
if (!a.url) return false;
|
|
||||||
if (!previewAssetUrl) return true;
|
|
||||||
try {
|
|
||||||
return decodeURIComponent(previewAssetUrl) !== a.url && a.url !== main;
|
|
||||||
} catch {
|
|
||||||
return a.url !== main;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Prefer showing all format variants in zoom for Meta carousel / Google PMax
|
|
||||||
const zoomImages =
|
const zoomImages =
|
||||||
otherImages.length > 0
|
ad.platform === 'google'
|
||||||
? otherImages
|
? images.filter((a) => a.url)
|
||||||
: images.filter((a) => a.url && proxiedMediaUrl(a.url) !== main);
|
: images.filter((a) => {
|
||||||
|
if (!a.url) return false;
|
||||||
|
const previewAssetUrl = main.replace(
|
||||||
|
/^\/api\/ad-tracker\/media\?url=/,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
if (!previewAssetUrl) return true;
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
decodeURIComponent(previewAssetUrl) !== a.url && a.url !== main
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return a.url !== main;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKey = (e: KeyboardEvent) => {
|
const onKey = (e: KeyboardEvent) => {
|
||||||
@@ -714,7 +725,9 @@ export function CreativeZoomModal({
|
|||||||
{zoomImages.length > 0 ? (
|
{zoomImages.length > 0 ? (
|
||||||
<section>
|
<section>
|
||||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||||
Otras piezas ({zoomImages.length})
|
{ad.platform === 'google'
|
||||||
|
? `Imágenes y logos (${zoomImages.length})`
|
||||||
|
: `Otras piezas (${zoomImages.length})`}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
{zoomImages.map((asset, i) => {
|
{zoomImages.map((asset, i) => {
|
||||||
@@ -810,7 +823,7 @@ export function CreativeZoomModal({
|
|||||||
Titulares largos ({longH.length})
|
Titulares largos ({longH.length})
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-1.5 text-sm text-gray-800">
|
<ul className="space-y-1.5 text-sm text-gray-800">
|
||||||
{longH.slice(0, 5).map((h) => (
|
{longH.map((h) => (
|
||||||
<li
|
<li
|
||||||
key={h}
|
key={h}
|
||||||
className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2"
|
className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2"
|
||||||
@@ -857,6 +870,89 @@ export function CreativeZoomModal({
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{ad.businessName ? (
|
||||||
|
<section>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||||
|
Nombre de negocio
|
||||||
|
</h3>
|
||||||
|
<p className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-800">
|
||||||
|
{ad.businessName}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{(ad.ctas || []).length > 0 ? (
|
||||||
|
<section>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||||
|
CTA ({ad.ctas!.length})
|
||||||
|
</h3>
|
||||||
|
<ul className="flex flex-wrap gap-1.5">
|
||||||
|
{ad.ctas!.map((c) => (
|
||||||
|
<li
|
||||||
|
key={c}
|
||||||
|
className="rounded-full border border-gray-200 bg-white px-3 py-1 text-xs text-gray-800"
|
||||||
|
>
|
||||||
|
{c}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{(ad.sitelinks || []).length > 0 ? (
|
||||||
|
<section>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||||
|
Sitelinks ({ad.sitelinks!.length})
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-1.5 text-sm text-gray-800">
|
||||||
|
{ad.sitelinks!.map((s) => (
|
||||||
|
<li
|
||||||
|
key={s}
|
||||||
|
className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2"
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{(ad.callouts || []).length > 0 ? (
|
||||||
|
<section>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||||
|
Callouts ({ad.callouts!.length})
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-1.5 text-sm text-gray-800">
|
||||||
|
{ad.callouts!.map((s) => (
|
||||||
|
<li
|
||||||
|
key={s}
|
||||||
|
className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2"
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{(ad.snippets || []).length > 0 ? (
|
||||||
|
<section>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
||||||
|
Structured snippets ({ad.snippets!.length})
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-1.5 text-sm text-gray-800">
|
||||||
|
{ad.snippets!.map((s) => (
|
||||||
|
<li
|
||||||
|
key={s}
|
||||||
|
className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2"
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import type {
|
|||||||
CreativeAsset,
|
CreativeAsset,
|
||||||
GoogleCreativeChannel,
|
GoogleCreativeChannel,
|
||||||
} from '@/lib/ad-creatives-meta';
|
} from '@/lib/ad-creatives-meta';
|
||||||
import { aggregateDailyByAd } from '@/lib/ad-creatives-meta';
|
import {
|
||||||
|
aggregateDailyByAd,
|
||||||
|
inferCreativeAspect,
|
||||||
|
} from '@/lib/ad-creatives-meta';
|
||||||
import {
|
import {
|
||||||
friendlyGoogleCreativeWarning,
|
friendlyGoogleCreativeWarning,
|
||||||
resolveGoogleAdsAccessToken,
|
resolveGoogleAdsAccessToken,
|
||||||
@@ -58,13 +61,23 @@ type GaqlRow = {
|
|||||||
asset?: {
|
asset?: {
|
||||||
id?: string;
|
id?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
name?: string;
|
||||||
textAsset?: { text?: string };
|
textAsset?: { text?: string };
|
||||||
imageAsset?: { fullSize?: { url?: string } };
|
imageAsset?: {
|
||||||
|
fullSize?: { url?: string; widthPixels?: number; heightPixels?: number };
|
||||||
|
};
|
||||||
youtubeVideoAsset?: {
|
youtubeVideoAsset?: {
|
||||||
youtubeVideoId?: string;
|
youtubeVideoId?: string;
|
||||||
youtubeVideoTitle?: string;
|
youtubeVideoTitle?: string;
|
||||||
};
|
};
|
||||||
callToActionAsset?: { callToAction?: string };
|
callToActionAsset?: { callToAction?: string };
|
||||||
|
sitelinkAsset?: {
|
||||||
|
linkText?: string;
|
||||||
|
description1?: string;
|
||||||
|
description2?: string;
|
||||||
|
};
|
||||||
|
calloutAsset?: { calloutText?: string };
|
||||||
|
structuredSnippetAsset?: { header?: string; values?: string[] };
|
||||||
};
|
};
|
||||||
metrics?: {
|
metrics?: {
|
||||||
costMicros?: string;
|
costMicros?: string;
|
||||||
@@ -211,9 +224,14 @@ function ytThumb(youtubeId: string): string {
|
|||||||
|
|
||||||
function googleAspectFromFieldType(ft: string): CreativeAspect {
|
function googleAspectFromFieldType(ft: string): CreativeAspect {
|
||||||
const u = ft.toUpperCase();
|
const u = ft.toUpperCase();
|
||||||
if (u.includes('PORTRAIT') || u.includes('TALL')) return '4:5';
|
if (u.includes('TALL_PORTRAIT') || u.includes('VERTICAL')) return '9:16';
|
||||||
if (u.includes('SQUARE') || u === 'LOGO') return '1:1';
|
if (u.includes('PORTRAIT')) return '4:5';
|
||||||
if (u.includes('YOUTUBE') || u.includes('LANDSCAPE') || u === 'MARKETING_IMAGE') {
|
if (u.includes('SQUARE') || u === 'LOGO' || u === 'BUSINESS_LOGO') return '1:1';
|
||||||
|
if (
|
||||||
|
u.includes('YOUTUBE') ||
|
||||||
|
u.includes('LANDSCAPE') ||
|
||||||
|
u === 'MARKETING_IMAGE'
|
||||||
|
) {
|
||||||
return '16:9';
|
return '16:9';
|
||||||
}
|
}
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
@@ -235,11 +253,6 @@ function pickVerticalGoogle(assets: CreativeAsset[]): string {
|
|||||||
return images[0]?.url || '';
|
return images[0]?.url || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function withAspect(asset: CreativeAsset, ft: string): CreativeAsset {
|
|
||||||
if (asset.aspect && asset.aspect !== 'unknown') return asset;
|
|
||||||
return { ...asset, aspect: googleAspectFromFieldType(ft) };
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Keep Search / PMax / Display represented in the gallery, not only top spend. */
|
/** Keep Search / PMax / Display represented in the gallery, not only top spend. */
|
||||||
function balanceGoogleCards(cards: AdCreativeCard[], limit: number): AdCreativeCard[] {
|
function balanceGoogleCards(cards: AdCreativeCard[], limit: number): AdCreativeCard[] {
|
||||||
const buckets: Record<string, AdCreativeCard[]> = {
|
const buckets: Record<string, AdCreativeCard[]> = {
|
||||||
@@ -259,7 +272,7 @@ function balanceGoogleCards(cards: AdCreativeCard[], limit: number): AdCreativeC
|
|||||||
const quotas: Record<string, number> = {
|
const quotas: Record<string, number> = {
|
||||||
search_brand: Math.max(8, Math.floor(limit * 0.2)),
|
search_brand: Math.max(8, Math.floor(limit * 0.2)),
|
||||||
search_generic: Math.max(8, Math.floor(limit * 0.2)),
|
search_generic: Math.max(8, Math.floor(limit * 0.2)),
|
||||||
pmax: Math.max(12, Math.floor(limit * 0.35)),
|
pmax: Math.max(24, Math.floor(limit * 0.4)),
|
||||||
display: Math.max(8, Math.floor(limit * 0.2)),
|
display: Math.max(8, Math.floor(limit * 0.2)),
|
||||||
other: Math.max(4, Math.floor(limit * 0.1)),
|
other: Math.max(4, Math.floor(limit * 0.1)),
|
||||||
};
|
};
|
||||||
@@ -295,9 +308,103 @@ type AgBucket = {
|
|||||||
longHeadlines: string[];
|
longHeadlines: string[];
|
||||||
descriptions: string[];
|
descriptions: string[];
|
||||||
ctas: string[];
|
ctas: string[];
|
||||||
|
businessNames: string[];
|
||||||
|
sitelinks: string[];
|
||||||
|
callouts: string[];
|
||||||
|
snippets: string[];
|
||||||
assets: CreativeAsset[];
|
assets: CreativeAsset[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function pushGoogleImage(
|
||||||
|
list: CreativeAsset[],
|
||||||
|
img: string,
|
||||||
|
ft: string,
|
||||||
|
kind: CreativeAsset['kind'],
|
||||||
|
width?: number,
|
||||||
|
height?: number
|
||||||
|
) {
|
||||||
|
if (!img || list.some((a) => a.url === img)) return;
|
||||||
|
const fromPx = inferCreativeAspect(ft, width, height);
|
||||||
|
const fromFt = googleAspectFromFieldType(ft);
|
||||||
|
list.push({
|
||||||
|
kind,
|
||||||
|
url: img,
|
||||||
|
label: ft || 'IMAGE',
|
||||||
|
aspect: fromPx !== 'unknown' ? fromPx : fromFt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushGoogleYoutube(
|
||||||
|
list: CreativeAsset[],
|
||||||
|
yt: string,
|
||||||
|
title?: string
|
||||||
|
) {
|
||||||
|
if (!yt || list.some((a) => a.youtubeId === yt)) return;
|
||||||
|
list.push({
|
||||||
|
kind: 'youtube',
|
||||||
|
url: ytThumb(yt),
|
||||||
|
label: 'YOUTUBE_VIDEO',
|
||||||
|
youtubeId: yt,
|
||||||
|
title: title || yt,
|
||||||
|
aspect: '16:9',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const PMAX_ASSET_QUERY = `
|
||||||
|
SELECT
|
||||||
|
campaign.id,
|
||||||
|
campaign.name,
|
||||||
|
campaign.advertising_channel_type,
|
||||||
|
asset_group.id,
|
||||||
|
asset_group.name,
|
||||||
|
asset_group.status,
|
||||||
|
asset_group.final_urls,
|
||||||
|
asset_group_asset.field_type,
|
||||||
|
asset.id,
|
||||||
|
asset.type,
|
||||||
|
asset.name,
|
||||||
|
asset.text_asset.text,
|
||||||
|
asset.image_asset.full_size.url,
|
||||||
|
asset.image_asset.full_size.width_pixels,
|
||||||
|
asset.image_asset.full_size.height_pixels,
|
||||||
|
asset.youtube_video_asset.youtube_video_id,
|
||||||
|
asset.youtube_video_asset.youtube_video_title,
|
||||||
|
asset.call_to_action_asset.call_to_action,
|
||||||
|
asset.sitelink_asset.link_text,
|
||||||
|
asset.sitelink_asset.description1,
|
||||||
|
asset.sitelink_asset.description2,
|
||||||
|
asset.callout_asset.callout_text,
|
||||||
|
asset.structured_snippet_asset.header,
|
||||||
|
asset.structured_snippet_asset.values
|
||||||
|
FROM asset_group_asset
|
||||||
|
WHERE asset_group.status != 'REMOVED'
|
||||||
|
AND asset_group_asset.status != 'REMOVED'
|
||||||
|
`;
|
||||||
|
|
||||||
|
const PMAX_ASSET_QUERY_BASIC = `
|
||||||
|
SELECT
|
||||||
|
campaign.id,
|
||||||
|
campaign.name,
|
||||||
|
campaign.advertising_channel_type,
|
||||||
|
asset_group.id,
|
||||||
|
asset_group.name,
|
||||||
|
asset_group.status,
|
||||||
|
asset_group.final_urls,
|
||||||
|
asset_group_asset.field_type,
|
||||||
|
asset.id,
|
||||||
|
asset.type,
|
||||||
|
asset.text_asset.text,
|
||||||
|
asset.image_asset.full_size.url,
|
||||||
|
asset.image_asset.full_size.width_pixels,
|
||||||
|
asset.image_asset.full_size.height_pixels,
|
||||||
|
asset.youtube_video_asset.youtube_video_id,
|
||||||
|
asset.youtube_video_asset.youtube_video_title,
|
||||||
|
asset.call_to_action_asset.call_to_action
|
||||||
|
FROM asset_group_asset
|
||||||
|
WHERE asset_group.status != 'REMOVED'
|
||||||
|
AND asset_group_asset.status != 'REMOVED'
|
||||||
|
`;
|
||||||
|
|
||||||
function cardsFromDaily(
|
function cardsFromDaily(
|
||||||
dailyRows: Parameters<typeof aggregateDailyByAd>[0],
|
dailyRows: Parameters<typeof aggregateDailyByAd>[0],
|
||||||
since: string,
|
since: string,
|
||||||
@@ -398,30 +505,8 @@ export async function fetchGoogleCreativeCards(opts: {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const [agAssets, agMetrics, adRows, adMetrics, adAssets] = await Promise.all([
|
const [agAssets, agMetrics, adRows, adMetrics, adAssets] = await Promise.all([
|
||||||
searchGoogle(
|
searchGoogle(customerId, accessToken, PMAX_ASSET_QUERY).catch(() =>
|
||||||
customerId,
|
searchGoogle(customerId, accessToken, PMAX_ASSET_QUERY_BASIC)
|
||||||
accessToken,
|
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
campaign.id,
|
|
||||||
campaign.name,
|
|
||||||
campaign.advertising_channel_type,
|
|
||||||
asset_group.id,
|
|
||||||
asset_group.name,
|
|
||||||
asset_group.status,
|
|
||||||
asset_group.final_urls,
|
|
||||||
asset_group_asset.field_type,
|
|
||||||
asset.id,
|
|
||||||
asset.type,
|
|
||||||
asset.text_asset.text,
|
|
||||||
asset.image_asset.full_size.url,
|
|
||||||
asset.youtube_video_asset.youtube_video_id,
|
|
||||||
asset.youtube_video_asset.youtube_video_title,
|
|
||||||
asset.call_to_action_asset.call_to_action
|
|
||||||
FROM asset_group_asset
|
|
||||||
WHERE asset_group.status != 'REMOVED'
|
|
||||||
AND asset_group_asset.status != 'REMOVED'
|
|
||||||
`
|
|
||||||
),
|
),
|
||||||
searchGoogle(
|
searchGoogle(
|
||||||
customerId,
|
customerId,
|
||||||
@@ -466,7 +551,6 @@ SELECT
|
|||||||
FROM ad_group_ad
|
FROM ad_group_ad
|
||||||
WHERE ad_group_ad.status != 'REMOVED'
|
WHERE ad_group_ad.status != 'REMOVED'
|
||||||
AND campaign.status != 'REMOVED'
|
AND campaign.status != 'REMOVED'
|
||||||
LIMIT 120
|
|
||||||
`
|
`
|
||||||
),
|
),
|
||||||
searchGoogle(
|
searchGoogle(
|
||||||
@@ -500,11 +584,13 @@ WHERE ad_group_ad.status != 'REMOVED'
|
|||||||
'MARKETING_IMAGE',
|
'MARKETING_IMAGE',
|
||||||
'SQUARE_MARKETING_IMAGE',
|
'SQUARE_MARKETING_IMAGE',
|
||||||
'PORTRAIT_MARKETING_IMAGE',
|
'PORTRAIT_MARKETING_IMAGE',
|
||||||
|
'TALL_PORTRAIT_MARKETING_IMAGE',
|
||||||
'LOGO',
|
'LOGO',
|
||||||
'LANDSCAPE_LOGO',
|
'LANDSCAPE_LOGO',
|
||||||
|
'BUSINESS_LOGO',
|
||||||
|
'AD_IMAGE',
|
||||||
'YOUTUBE_VIDEO'
|
'YOUTUBE_VIDEO'
|
||||||
)
|
)
|
||||||
LIMIT 400
|
|
||||||
`
|
`
|
||||||
).catch(() => [] as GaqlRow[]),
|
).catch(() => [] as GaqlRow[]),
|
||||||
]);
|
]);
|
||||||
@@ -516,30 +602,21 @@ LIMIT 400
|
|||||||
const list = assetsByAdId.get(adId) || [];
|
const list = assetsByAdId.get(adId) || [];
|
||||||
const ft = enumName(row.adGroupAdAssetView?.fieldType).toUpperCase();
|
const ft = enumName(row.adGroupAdAssetView?.fieldType).toUpperCase();
|
||||||
const img = str(row.asset?.imageAsset?.fullSize?.url);
|
const img = str(row.asset?.imageAsset?.fullSize?.url);
|
||||||
|
const w = n(row.asset?.imageAsset?.fullSize?.widthPixels) || undefined;
|
||||||
|
const h = n(row.asset?.imageAsset?.fullSize?.heightPixels) || undefined;
|
||||||
const yt = str(row.asset?.youtubeVideoAsset?.youtubeVideoId);
|
const yt = str(row.asset?.youtubeVideoAsset?.youtubeVideoId);
|
||||||
const ytTitle = str(row.asset?.youtubeVideoAsset?.youtubeVideoTitle);
|
const ytTitle = str(row.asset?.youtubeVideoAsset?.youtubeVideoTitle);
|
||||||
if (img && !list.some((a) => a.url === img)) {
|
if (img) {
|
||||||
list.push(
|
pushGoogleImage(
|
||||||
withAspect(
|
list,
|
||||||
{
|
img,
|
||||||
kind: ft.includes('LOGO') ? 'logo' : 'image',
|
ft || 'MARKETING_IMAGE',
|
||||||
url: img,
|
ft.includes('LOGO') ? 'logo' : 'image',
|
||||||
label: ft || 'MARKETING_IMAGE',
|
w,
|
||||||
},
|
h
|
||||||
ft || 'MARKETING_IMAGE'
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (yt && !list.some((a) => a.youtubeId === yt)) {
|
if (yt) pushGoogleYoutube(list, yt, ytTitle);
|
||||||
list.push({
|
|
||||||
kind: 'youtube',
|
|
||||||
url: ytThumb(yt),
|
|
||||||
label: 'YOUTUBE_VIDEO',
|
|
||||||
youtubeId: yt,
|
|
||||||
title: ytTitle || yt,
|
|
||||||
aspect: '16:9',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
assetsByAdId.set(adId, list);
|
assetsByAdId.set(adId, list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -613,6 +690,10 @@ LIMIT 400
|
|||||||
longHeadlines: [],
|
longHeadlines: [],
|
||||||
descriptions: [],
|
descriptions: [],
|
||||||
ctas: [],
|
ctas: [],
|
||||||
|
businessNames: [],
|
||||||
|
sitelinks: [],
|
||||||
|
callouts: [],
|
||||||
|
snippets: [],
|
||||||
assets: [],
|
assets: [],
|
||||||
};
|
};
|
||||||
agMap.set(agid, entry);
|
agMap.set(agid, entry);
|
||||||
@@ -620,37 +701,51 @@ LIMIT 400
|
|||||||
const ft = enumName(row.assetGroupAsset?.fieldType).toUpperCase();
|
const ft = enumName(row.assetGroupAsset?.fieldType).toUpperCase();
|
||||||
const text = str(row.asset?.textAsset?.text);
|
const text = str(row.asset?.textAsset?.text);
|
||||||
const img = str(row.asset?.imageAsset?.fullSize?.url);
|
const img = str(row.asset?.imageAsset?.fullSize?.url);
|
||||||
|
const imgW = n(row.asset?.imageAsset?.fullSize?.widthPixels) || undefined;
|
||||||
|
const imgH = n(row.asset?.imageAsset?.fullSize?.heightPixels) || undefined;
|
||||||
const yt = str(row.asset?.youtubeVideoAsset?.youtubeVideoId);
|
const yt = str(row.asset?.youtubeVideoAsset?.youtubeVideoId);
|
||||||
const ytTitle = str(row.asset?.youtubeVideoAsset?.youtubeVideoTitle);
|
const ytTitle = str(row.asset?.youtubeVideoAsset?.youtubeVideoTitle);
|
||||||
const cta = enumName(row.asset?.callToActionAsset?.callToAction);
|
const cta = enumName(row.asset?.callToActionAsset?.callToAction);
|
||||||
|
const sitelink = row.asset?.sitelinkAsset;
|
||||||
|
const callout = str(row.asset?.calloutAsset?.calloutText);
|
||||||
|
const snippet = row.asset?.structuredSnippetAsset;
|
||||||
|
|
||||||
if (ft === 'HEADLINE' && text) entry.headlines.push(text);
|
if ((ft === 'HEADLINE' || ft === 'MANDATORY_AD_TEXT') && text) {
|
||||||
else if (ft === 'LONG_HEADLINE' && text) entry.longHeadlines.push(text);
|
entry.headlines.push(text);
|
||||||
else if (ft === 'DESCRIPTION' && text) entry.descriptions.push(text);
|
} else if (ft === 'LONG_HEADLINE' && text) {
|
||||||
else if (ft === 'CALL_TO_ACTION_SELECTION' && cta) entry.ctas.push(cta);
|
entry.longHeadlines.push(text);
|
||||||
else if (ft.includes('MARKETING_IMAGE') && img) {
|
} else if (ft === 'DESCRIPTION' && text) {
|
||||||
if (!entry.assets.some((a) => a.url === img)) {
|
entry.descriptions.push(text);
|
||||||
entry.assets.push(
|
} else if (ft === 'BUSINESS_NAME' && text) {
|
||||||
withAspect({ kind: 'image', url: img, label: ft }, ft)
|
entry.businessNames.push(text);
|
||||||
);
|
} else if (ft === 'CALL_TO_ACTION_SELECTION' && cta) {
|
||||||
}
|
entry.ctas.push(cta);
|
||||||
} else if ((ft === 'LOGO' || ft === 'LANDSCAPE_LOGO') && img) {
|
} else if (ft === 'SITELINK' && sitelink) {
|
||||||
if (!entry.assets.some((a) => a.url === img)) {
|
const line = [
|
||||||
entry.assets.push(
|
str(sitelink.linkText),
|
||||||
withAspect({ kind: 'logo', url: img, label: ft }, ft)
|
str(sitelink.description1),
|
||||||
);
|
str(sitelink.description2),
|
||||||
}
|
]
|
||||||
} else if (ft === 'YOUTUBE_VIDEO' && yt) {
|
.filter(Boolean)
|
||||||
if (!entry.assets.some((a) => a.youtubeId === yt)) {
|
.join(' · ');
|
||||||
entry.assets.push({
|
if (line) entry.sitelinks.push(line);
|
||||||
kind: 'youtube',
|
} else if (ft === 'CALLOUT' && callout) {
|
||||||
url: ytThumb(yt),
|
entry.callouts.push(callout);
|
||||||
label: 'YOUTUBE_VIDEO',
|
} else if (ft === 'STRUCTURED_SNIPPET' && snippet) {
|
||||||
youtubeId: yt,
|
const vals = (snippet.values || []).map(str).filter(Boolean).join(', ');
|
||||||
title: ytTitle || yt,
|
const line = [str(snippet.header), vals].filter(Boolean).join(': ');
|
||||||
aspect: '16:9',
|
if (line) entry.snippets.push(line);
|
||||||
});
|
} else if (img && (/IMAGE|LOGO/.test(ft) || row.asset?.type === 'IMAGE')) {
|
||||||
}
|
pushGoogleImage(
|
||||||
|
entry.assets,
|
||||||
|
img,
|
||||||
|
ft || 'IMAGE',
|
||||||
|
/LOGO/.test(ft) ? 'logo' : 'image',
|
||||||
|
imgW,
|
||||||
|
imgH
|
||||||
|
);
|
||||||
|
} else if (yt && (/VIDEO/.test(ft) || row.asset?.type === 'YOUTUBE_VIDEO')) {
|
||||||
|
pushGoogleYoutube(entry.assets, yt, ytTitle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,11 +760,25 @@ LIMIT 400
|
|||||||
const impressions = m?.impressions ?? camp?.impressions ?? 0;
|
const impressions = m?.impressions ?? camp?.impressions ?? 0;
|
||||||
const clicks = m?.clicks ?? camp?.clicks ?? 0;
|
const clicks = m?.clicks ?? camp?.clicks ?? 0;
|
||||||
const conversions = m?.conversions ?? camp?.conversions ?? 0;
|
const conversions = m?.conversions ?? camp?.conversions ?? 0;
|
||||||
if (spend <= 0 && impressions <= 0 && clicks <= 0 && !m) continue;
|
const isPmax =
|
||||||
|
/PERFORMANCE_MAX/i.test(entry.channelType) ||
|
||||||
|
/P[\s_-]?MAX|PERFORMANCE[\s_-]?MAX/i.test(entry.campaignName);
|
||||||
|
const hasPack =
|
||||||
|
entry.assets.length > 0 ||
|
||||||
|
entry.headlines.length > 0 ||
|
||||||
|
entry.descriptions.length > 0;
|
||||||
|
if (spend <= 0 && impressions <= 0 && clicks <= 0 && !m && !(isPmax && hasPack)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const headlines = uniq(entry.headlines);
|
const headlines = uniq(entry.headlines);
|
||||||
const longHeadlines = uniq(entry.longHeadlines);
|
const longHeadlines = uniq(entry.longHeadlines);
|
||||||
const descriptions = uniq(entry.descriptions);
|
const descriptions = uniq(entry.descriptions);
|
||||||
|
const sitelinks = uniq(entry.sitelinks);
|
||||||
|
const callouts = uniq(entry.callouts);
|
||||||
|
const snippets = uniq(entry.snippets);
|
||||||
|
const ctas = uniq(entry.ctas);
|
||||||
|
const businessName = uniq(entry.businessNames)[0];
|
||||||
const preview =
|
const preview =
|
||||||
pickVerticalGoogle(entry.assets) ||
|
pickVerticalGoogle(entry.assets) ||
|
||||||
entry.assets.find((a) => a.kind === 'youtube')?.url ||
|
entry.assets.find((a) => a.kind === 'youtube')?.url ||
|
||||||
@@ -701,9 +810,14 @@ LIMIT 400
|
|||||||
bodies: descriptions,
|
bodies: descriptions,
|
||||||
longHeadlines,
|
longHeadlines,
|
||||||
descriptions: [],
|
descriptions: [],
|
||||||
|
sitelinks,
|
||||||
|
callouts,
|
||||||
|
snippets,
|
||||||
|
ctas,
|
||||||
assets: entry.assets,
|
assets: entry.assets,
|
||||||
linkUrl: entry.finalUrls[0],
|
linkUrl: entry.finalUrls[0],
|
||||||
cta: uniq(entry.ctas)[0],
|
cta: ctas[0],
|
||||||
|
businessName,
|
||||||
status: entry.status,
|
status: entry.status,
|
||||||
youtubeId: firstYt,
|
youtubeId: firstYt,
|
||||||
});
|
});
|
||||||
@@ -827,6 +941,8 @@ LIMIT 400
|
|||||||
c.impressions > 0 ||
|
c.impressions > 0 ||
|
||||||
c.clicks > 0 ||
|
c.clicks > 0 ||
|
||||||
c.conversions > 0 ||
|
c.conversions > 0 ||
|
||||||
|
(c.mediaType === 'pmax' &&
|
||||||
|
((c.assets?.length || 0) > 0 || (c.headlines?.length || 0) > 0)) ||
|
||||||
(c.mediaType === 'rsa' && (c.headlines?.length || 0) > 0) ||
|
(c.mediaType === 'rsa' && (c.headlines?.length || 0) > 0) ||
|
||||||
((c.mediaType === 'rda' || c.mediaType === 'image') &&
|
((c.mediaType === 'rda' || c.mediaType === 'image') &&
|
||||||
(c.assets?.length || 0) > 0)
|
(c.assets?.length || 0) > 0)
|
||||||
|
|||||||
@@ -61,13 +61,17 @@ export type AdCreativeCard = {
|
|||||||
/** Joined for backwards compat; prefer headlines/bodies/descriptions arrays. */
|
/** Joined for backwards compat; prefer headlines/bodies/descriptions arrays. */
|
||||||
headline: string;
|
headline: string;
|
||||||
primaryText: string;
|
primaryText: string;
|
||||||
/** Titulares (Meta titles / Google headlines), máx. 5. */
|
/** Titulares (Meta titles / Google headlines). */
|
||||||
headlines: string[];
|
headlines: string[];
|
||||||
/** Textos principales (Meta bodies), máx. 5. */
|
/** Textos principales (Meta bodies / Google descriptions). */
|
||||||
bodies: string[];
|
bodies: string[];
|
||||||
/** Descripción del enlace (Meta descriptions), suele ser 1. */
|
/** Descripción del enlace (Meta) u otros textos cortos. */
|
||||||
descriptions: string[];
|
descriptions: string[];
|
||||||
longHeadlines?: string[];
|
longHeadlines?: string[];
|
||||||
|
sitelinks?: string[];
|
||||||
|
callouts?: string[];
|
||||||
|
snippets?: string[];
|
||||||
|
ctas?: string[];
|
||||||
assets: CreativeAsset[];
|
assets: CreativeAsset[];
|
||||||
linkUrl?: string;
|
linkUrl?: string;
|
||||||
cta?: string;
|
cta?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user