Script Google AdsHebdomadaireLecture seule
Termes de recherche Performance Max
Ce que vos campagnes Performance Max diffusent vraiment : catégories de recherche et placements, exportés chaque semaine dans un Google Sheet.
Le problème que ça règle
Performance Max ne montre presque rien de ce qu'elle achète : ni les requêtes qui déclenchent vos annonces, ni les sites et vidéos où elles s'affichent. Résultat, une part du budget part sur votre propre nom de marque, sur des produits hors périmètre ou sur des placements sans valeur, sans que rien ne le signale dans l'interface.
Ce que fait le script
- Liste les campagnes Performance Max actives du compte.
- Pour chacune, lit les catégories de termes de recherche (impressions, clics, conversions, coût) et les placements Display et YouTube.
- Écrit les deux rapports dans un Google Sheet (onglets « Search terms » et « Placements »), remplacés à chaque exécution.
- Envoie un résumé par e-mail avec les catégories les plus diffusées.
Installation
- Dans Google Ads : Outils et paramètres, Actions groupées, Scripts, puis le bouton « + ».
- Collez le code complet, remplacez les valeurs du bloc CONFIG en haut du script.
- Cliquez sur « Aperçu » : le script tourne sans rien envoyer et affiche son journal. Autorisez les accès demandés (Google Ads, e-mail, Google Sheets selon le script).
- Cliquez sur « Enregistrer », puis planifiez l’exécution avec l’icône horloge, à la fréquence indiquée.
Paramètres à adapter
| Paramètre | Rôle |
|---|---|
SPREADSHEET_URL | URL d'un Google Sheet vide créé au préalable |
EMAIL_RECIPIENTS | Adresses destinataires du résumé, ou [] pour ne rien envoyer |
DATE_RANGE | Période analysée : LAST_30_DAYS, LAST_7_DAYS, LAST_MONTH… |
EMAIL_TOP_N | Nombre de catégories affichées dans l'e-mail |
Le code
/**
* PMax Insights & Placements Report
* Par Pierre Bordier — https://pierrebordier.com
*
* Performance Max ne donne quasiment aucune visibilité native sur les
* search terms ni sur les placements Display/YouTube consommés par les
* campagnes. Ce script lit ces deux rapports (lecture seule, aucune
* modification du compte) et les publie dans un Google Sheet, avec un
* résumé envoyé par e-mail.
*
* INSTALLATION
* 1. Google Ads > Outils et paramètres > Actions groupées > Scripts > "+".
* 2. Collez tout ce fichier, remplacez SPREADSHEET_URL et EMAIL_RECIPIENTS
* ci-dessous par vos propres valeurs.
* 3. Créez d'abord un Google Sheet vide, copiez son URL dans SPREADSHEET_URL
* (le script crée/écrase deux onglets dedans : "Search terms" et
* "Placements" — ne réutilisez pas un Sheet contenant des données à
* garder).
* 4. "Aperçu" pour un premier test, puis "Enregistrer".
* 5. Planifiez l'exécution (icône horloge) : hebdomadaire est un bon
* rythme par défaut.
*
* LIMITES CONNUES
* - "campaign_search_term_insight" est une ressource assez récente de
* l'API Google Ads : Google peut renvoyer des métriques partielles
* (impressions seules, sans clics/coût) sur certaines catégories, par
* seuil de confidentialité. C'est un comportement normal côté Google,
* pas un bug du script.
* - Si Google modifie les champs disponibles sur "performance_max_placement_view"
* ou "campaign_search_term_insight", ce script peut renvoyer une erreur
* "field not compatible" ponctuelle. Vérifiez alors le schéma à jour sur
* https://developers.google.com/google-ads/api/fields/latest/overview
* et ajustez les champs du SELECT correspondant — chaque rapport est
* isolé dans son propre try/catch, donc une erreur sur l'un n'empêche
* pas l'autre de s'exécuter.
*/
const CONFIG = {
// Collez ici l'URL d'un Google Sheet VIDE que vous avez créé au préalable.
SPREADSHEET_URL: 'https://docs.google.com/spreadsheets/d/VOTRE_ID_ICI/edit',
// Une ou plusieurs adresses, séparées par des virgules. Laissez [] pour
// désactiver l'envoi d'e-mail (le Sheet sera quand même mis à jour).
EMAIL_RECIPIENTS: ['vous@exemple.com'],
// Période analysée à chaque exécution. Valeurs possibles : LAST_7_DAYS,
// LAST_14_DAYS, LAST_30_DAYS, LAST_MONTH, THIS_MONTH...
DATE_RANGE: 'LAST_30_DAYS',
// Nombre de catégories affichées dans le résumé e-mail (le Sheet, lui,
// contient toujours la liste complète).
EMAIL_TOP_N: 10,
};
function main() {
const spreadsheet = openConfiguredSpreadsheet_();
const campaigns = getPerformanceMaxCampaigns_();
if (campaigns.length === 0) {
Logger.log('Aucune campagne Performance Max active trouvée sur ce compte. Fin du script.');
return;
}
Logger.log(campaigns.length + ' campagne(s) Performance Max trouvée(s) : ' + campaigns.map((c) => c.name).join(', '));
const searchTermRows = [];
const placementRows = [];
campaigns.forEach((campaign) => {
try {
searchTermRows.push(...getSearchTermInsights_(campaign));
} catch (e) {
Logger.log('[Search terms] échec pour la campagne "' + campaign.name + '" : ' + e);
}
try {
placementRows.push(...getPlacementInsights_(campaign));
} catch (e) {
Logger.log('[Placements] échec pour la campagne "' + campaign.name + '" : ' + e);
}
});
writeSheet_(spreadsheet, 'Search terms', [
'Campagne', 'Catégorie de recherche', 'Impressions', 'Clics', 'Conversions', 'Valeur conv.', 'Coût',
], searchTermRows);
writeSheet_(spreadsheet, 'Placements', [
'Campagne', 'Type', 'Placement', 'Impressions', 'Clics', 'Coût',
], placementRows);
sendSummaryEmail_(spreadsheet, campaigns, searchTermRows, placementRows);
Logger.log('Terminé. ' + searchTermRows.length + ' lignes search terms, ' + placementRows.length + ' lignes placements. Sheet : ' + spreadsheet.getUrl());
}
function openConfiguredSpreadsheet_() {
if (!CONFIG.SPREADSHEET_URL || CONFIG.SPREADSHEET_URL.indexOf('VOTRE_ID_ICI') !== -1) {
throw new Error(
"CONFIG.SPREADSHEET_URL n'est pas configuré. Créez un Google Sheet vide, copiez son URL en haut de ce script.",
);
}
return SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL);
}
function getPerformanceMaxCampaigns_() {
const campaigns = [];
const rows = AdsApp.search(`
SELECT campaign.id, campaign.name
FROM campaign
WHERE campaign.advertising_channel_type = 'PERFORMANCE_MAX'
AND campaign.status = 'ENABLED'
`);
while (rows.hasNext()) {
const row = rows.next();
campaigns.push({ id: row.campaign.id, name: row.campaign.name });
}
return campaigns;
}
// "campaign_search_term_insight" exige de filtrer sur UNE seule campagne à
// la fois (contrainte de l'API Google Ads pour cette ressource) : on boucle
// donc campagne par campagne plutôt que de tout récupérer en une requête.
function getSearchTermInsights_(campaign) {
const rows = AdsApp.search(`
SELECT
campaign_search_term_insight.category_label,
metrics.impressions,
metrics.clicks,
metrics.conversions,
metrics.conversions_value,
metrics.cost_micros
FROM campaign_search_term_insight
WHERE campaign_search_term_insight.campaign_id = ${campaign.id}
AND segments.date DURING ${CONFIG.DATE_RANGE}
ORDER BY metrics.impressions DESC
`);
const out = [];
while (rows.hasNext()) {
const row = rows.next();
out.push([
campaign.name,
row.campaignSearchTermInsight.categoryLabel || '(non catégorisé)',
row.metrics.impressions || 0,
row.metrics.clicks || 0,
row.metrics.conversions || 0,
row.metrics.conversionsValue || 0,
microsToUnits_(row.metrics.costMicros),
]);
}
return out;
}
function getPlacementInsights_(campaign) {
const rows = AdsApp.search(`
SELECT
performance_max_placement_view.placement_type,
performance_max_placement_view.display_name,
performance_max_placement_view.target_url,
metrics.impressions,
metrics.clicks,
metrics.cost_micros
FROM performance_max_placement_view
WHERE campaign.id = ${campaign.id}
AND segments.date DURING ${CONFIG.DATE_RANGE}
ORDER BY metrics.impressions DESC
`);
const out = [];
while (rows.hasNext()) {
const row = rows.next();
const view = row.performanceMaxPlacementView;
out.push([
campaign.name,
view.placementType || '',
view.displayName || view.targetUrl || '(inconnu)',
row.metrics.impressions || 0,
row.metrics.clicks || 0,
microsToUnits_(row.metrics.costMicros),
]);
}
return out;
}
function microsToUnits_(micros) {
return micros ? Math.round((micros / 1e6) * 100) / 100 : 0;
}
function writeSheet_(spreadsheet, sheetName, headers, rows) {
let sheet = spreadsheet.getSheetByName(sheetName);
if (!sheet) sheet = spreadsheet.insertSheet(sheetName);
sheet.clearContents();
sheet.getRange(1, 1, 1, headers.length).setValues([headers]).setFontWeight('bold');
sheet.setFrozenRows(1);
if (rows.length > 0) {
sheet.getRange(2, 1, rows.length, headers.length).setValues(rows);
}
sheet.autoResizeColumns(1, headers.length);
}
function sendSummaryEmail_(spreadsheet, campaigns, searchTermRows, placementRows) {
if (!CONFIG.EMAIL_RECIPIENTS || CONFIG.EMAIL_RECIPIENTS.length === 0) return;
const currency = AdsApp.currentAccount().getCurrencyCode();
const topCategories = searchTermRows.slice(0, CONFIG.EMAIL_TOP_N);
const today = new Date().toLocaleDateString('fr-CH');
let body = 'Rapport PMax Insights — ' + today + '\n';
body += 'Compte : ' + AdsApp.currentAccount().getName() + ' (' + AdsApp.currentAccount().getCustomerId() + ')\n';
body += 'Période : ' + CONFIG.DATE_RANGE + '\n\n';
body += campaigns.length + ' campagne(s) Performance Max analysée(s)\n';
body += searchTermRows.length + ' ligne(s) de catégories de search terms\n';
body += placementRows.length + ' placement(s) trouvé(s)\n\n';
if (topCategories.length > 0) {
body += 'Top ' + topCategories.length + ' catégories de search terms (par impressions) :\n';
topCategories.forEach((row) => {
body += '- ' + row[1] + ' (' + row[0] + ') : ' + row[2] + ' impr., ' + row[3] + ' clics, ' + row[6] + ' ' + currency + '\n';
});
body += '\n';
}
body += 'Détail complet : ' + spreadsheet.getUrl();
MailApp.sendEmail({
to: CONFIG.EMAIL_RECIPIENTS.join(','),
subject: 'Rapport PMax Insights - ' + today,
body: body,
});
}
Limites connues
- Google renvoie parfois des métriques partielles sur certaines catégories, par seuil de confidentialité : ce n’est pas un défaut du script.
- Le script liste, il n'exclut rien : vous décidez ligne par ligne des exclusions à poser au niveau du compte.