Script Google AdsHebdomadaire ou quotidienLecture seule

Rapport des changements de statut et de budget

Qui a mis en pause quoi, quand, et depuis quel outil : un récapitulatif des changements de statut et de budget, recommandations automatiques comprises.

Lecture seuleE-mailGoogle Sheet

Le problème que ça règle

Une campagne en pause depuis dix jours, un budget doublé, une recommandation Google appliquée automatiquement : l'historique des modifications contient tout, mais personne ne va le lire. Ce script le lit pour vous et signale ce qui n'est pas de votre main.

Ce que fait le script

  1. Lit l'historique des modifications du compte sur les N derniers jours (30 au maximum, limite de Google).
  2. Garde les changements de statut (campagnes, groupes d'annonces, annonces, mots-clés), de budget, les créations et suppressions.
  3. Indique pour chacun l'utilisateur et l'outil d'origine : interface, Editor, API, règle automatique, recommandation appliquée automatiquement, script.
  4. Envoie un récapitulatif par e-mail, en mettant en avant les changements automatiques, et l'ajoute à un Google Sheet si vous en indiquez un.

Installation

  1. Dans Google Ads : Outils et paramètres, Actions groupées, Scripts, puis le bouton « + ».
  2. Collez le code complet, remplacez les valeurs du bloc CONFIG en haut du script.
  3. 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).
  4. Cliquez sur « Enregistrer », puis planifiez l’exécution avec l’icône horloge, à la fréquence indiquée.

Paramètres à adapter

ParamètreRôle
DAYS_BACKJours d’historique analysés, de 1 à 29
EMAIL_RECIPIENTSAdresses destinataires, séparées par des virgules
SPREADSHEET_URLOptionnel : Google Sheet où archiver les changements
ONLY_IF_CHANGEStrue pour ne rien envoyer quand rien n’a bougé
INCLUDE_OTHER_CHANGEStrue pour inclure aussi enchères, annonces et ciblages modifiés

Le code

change-history-report.js
Télécharger
/**
 * Rapport des changements de statut et de budget (historique des modifications)
 * Par Pierre Bordier — https://pierrebordier.com
 *
 * Qui a mis en pause cette campagne ? Quand ce budget a-t-il doublé ? Une
 * recommandation a-t-elle été appliquée automatiquement ? Ce script lit
 * l'historique des modifications du compte sur les N derniers jours, garde les
 * changements de statut (campagnes, groupes d'annonces, annonces, mots-clés) et
 * de budget, indique qui les a faits et depuis quel outil (interface, Editor,
 * API, règle automatique, recommandations appliquées automatiquement, script),
 * puis envoie un récapitulatif par e-mail et, si vous le souhaitez, l'écrit dans
 * un Google Sheet. Lecture seule : il ne modifie jamais le compte.
 *
 * INSTALLATION
 * 1. Google Ads > Outils et paramètres > Actions groupées > Scripts > "+".
 * 2. Collez tout ce fichier, ajustez le bloc CONFIG ci-dessous.
 * 3. "Aperçu" pour un premier test, puis "Enregistrer".
 * 4. Planifiez l'exécution (icône horloge) : hebdomadaire avec DAYS_BACK = 7,
 *    ou quotidienne avec DAYS_BACK = 1.
 *
 * LIMITES CONNUES
 * - L'historique des modifications (ressource change_event) ne couvre que les
 *   30 derniers jours : DAYS_BACK est plafonné à 29.
 * - Google impose une limite de lignes par requête (10 000 maximum) : sur un
 *   compte très actif, augmentez la fréquence plutôt que DAYS_BACK.
 * - Les champs de change_event évoluent parfois côté Google ; en cas d'erreur
 *   "field not found", vérifiez le schéma sur
 *   https://developers.google.com/google-ads/api/fields/latest/change_event
 */

const CONFIG = {
  // Nombre de jours d'historique à analyser (1 à 29).
  DAYS_BACK: 7,

  // Destinataires, séparés par des virgules.
  EMAIL_RECIPIENTS: 'vous@entreprise.com',

  // Facultatif : URL d'un Google Sheet où ajouter les changements (un onglet
  // "Changements" est créé ou complété). Laissez vide pour ne pas l'utiliser.
  SPREADSHEET_URL: '',

  // true : n'envoie l'e-mail que s'il y a au moins un changement.
  ONLY_IF_CHANGES: true,

  // Inclure aussi les changements qui ne concernent ni un statut ni un budget
  // (enchères, annonces modifiées, ciblages...). false = statuts et budgets seulement.
  INCLUDE_OTHER_CHANGES: false,

  // Nombre maximal de lignes lues (plafond Google : 10000).
  MAX_ROWS: 5000,
};

// Libellés lisibles pour l'outil à l'origine du changement.
const CLIENT_LABELS = {
  GOOGLE_ADS_WEB_CLIENT: 'Interface Google Ads',
  GOOGLE_ADS_AUTOMATED_RULE: 'Règle automatique',
  GOOGLE_ADS_SCRIPTS: 'Script',
  GOOGLE_ADS_BULK_UPLOAD: 'Import groupé',
  GOOGLE_ADS_API: 'API',
  GOOGLE_ADS_EDITOR: 'Google Ads Editor',
  GOOGLE_ADS_MOBILE_APP: 'Application mobile',
  GOOGLE_ADS_RECOMMENDATIONS: 'Recommandation appliquée',
  GOOGLE_ADS_RECOMMENDATIONS_SUBSCRIPTION: 'Recommandation appliquée automatiquement',
  SEARCH_ADS_360_SYNC: 'Search Ads 360',
  SEARCH_ADS_360_POST: 'Search Ads 360',
  INTERNAL_TOOL: 'Outil interne Google',
  OTHER: 'Autre',
};

function main() {
  const account = AdsApp.currentAccount();
  const tz = account.getTimeZone();
  const days = Math.min(Math.max(CONFIG.DAYS_BACK, 1), 29);
  const now = new Date();
  const start = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
  const fromStr = Utilities.formatDate(start, tz, 'yyyy-MM-dd HH:mm:ss');
  const toStr = Utilities.formatDate(now, tz, 'yyyy-MM-dd HH:mm:ss');

  const query = [
    'SELECT change_event.change_date_time, change_event.change_resource_type,',
    '  change_event.resource_change_operation, change_event.changed_fields,',
    '  change_event.user_email, change_event.client_type,',
    '  change_event.old_resource, change_event.new_resource,',
    '  campaign.name, ad_group.name',
    'FROM change_event',
    "WHERE change_event.change_date_time >= '" + fromStr + "'",
    "  AND change_event.change_date_time <= '" + toStr + "'",
    'ORDER BY change_event.change_date_time DESC',
    'LIMIT ' + CONFIG.MAX_ROWS,
  ].join('\n');

  const changes = [];
  const rows = AdsApp.search(query);
  while (rows.hasNext()) {
    const row = rows.next();
    const ev = row.changeEvent || {};
    const changed = String(ev.changedFields || '');
    const isStatus = /(^|,)\s*(status|primary_status)\b/i.test(changed) || /\bstatus\b/i.test(changed);
    const isBudget = /amount_micros|budget/i.test(changed) || ev.changeResourceType === 'CAMPAIGN_BUDGET';
    const isRemoval = ev.resourceChangeOperation === 'REMOVE' || ev.resourceChangeOperation === 'CREATE';
    if (!isStatus && !isBudget && !isRemoval && !CONFIG.INCLUDE_OTHER_CHANGES) continue;

    const detail = describe(ev);
    changes.push({
      when: String(ev.changeDateTime || ''),
      type: String(ev.changeResourceType || ''),
      operation: String(ev.resourceChangeOperation || ''),
      campaign: (row.campaign && row.campaign.name) || '',
      adGroup: (row.adGroup && row.adGroup.name) || '',
      who: String(ev.userEmail || ''),
      tool: CLIENT_LABELS[ev.clientType] || String(ev.clientType || ''),
      detail: detail,
      fields: changed,
    });
  }

  Logger.log(changes.length + ' changement(s) retenu(s) sur ' + days + ' jour(s).');
  changes.slice(0, 20).forEach(function (c) {
    Logger.log(c.when + ' | ' + c.type + ' | ' + c.campaign + ' | ' + c.who + ' (' + c.tool + ') | ' + c.detail);
  });

  if (CONFIG.SPREADSHEET_URL) {
    try { appendToSheet(changes); } catch (e) { Logger.log('Google Sheet non mis à jour : ' + e); }
  }

  if (changes.length === 0 && CONFIG.ONLY_IF_CHANGES) {
    Logger.log('Aucun changement, pas d\'e-mail.');
    return;
  }
  const auto = changes.filter(function (c) { return /Recommandation|Règle/.test(c.tool); }).length;
  MailApp.sendEmail({
    to: CONFIG.EMAIL_RECIPIENTS,
    subject: '[Google Ads] ' + changes.length + ' changement(s) de statut ou budget sur ' + days + ' j - ' + account.getName()
      + (auto ? ' (' + auto + ' automatique(s))' : ''),
    htmlBody: buildHtml(account, changes, days, auto),
  });
  Logger.log('E-mail envoyé à ' + CONFIG.EMAIL_RECIPIENTS);
}

// Décrit le changement à partir des états avant/après selon le type de ressource.
function describe(ev) {
  const oldR = ev.oldResource || {};
  const newR = ev.newResource || {};
  const pairs = [
    ['campaign', 'status'], ['adGroup', 'status'], ['adGroupAd', 'status'], ['adGroupCriterion', 'status'],
    ['campaignBudget', 'amountMicros'], ['campaign', 'name'], ['adGroupCriterion', 'cpcBidMicros'],
  ];
  const parts = [];
  pairs.forEach(function (p) {
    const before = oldR[p[0]] ? oldR[p[0]][p[1]] : undefined;
    const after = newR[p[0]] ? newR[p[0]][p[1]] : undefined;
    if (before === undefined && after === undefined) return;
    if (String(before) === String(after)) return;
    parts.push(p[0] + '.' + p[1] + ' : ' + human(p[1], before) + ' -> ' + human(p[1], after));
  });
  if (parts.length) return parts.join(' ; ');
  return ev.resourceChangeOperation === 'REMOVE' ? 'suppression' : ev.resourceChangeOperation === 'CREATE' ? 'création' : 'champs : ' + String(ev.changedFields || '');
}

function human(field, value) {
  if (value === undefined || value === null) return '-';
  if (/Micros$/.test(field)) return (Number(value) / 1000000).toFixed(2);
  return String(value);
}

function appendToSheet(changes) {
  const ss = SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL);
  let sheet = ss.getSheetByName('Changements');
  if (!sheet) {
    sheet = ss.insertSheet('Changements');
    sheet.appendRow(['Date', 'Type', 'Opération', 'Campagne', 'Groupe d\'annonces', 'Utilisateur', 'Outil', 'Détail', 'Champs']);
  }
  const values = changes.map(function (c) {
    return [c.when, c.type, c.operation, c.campaign, c.adGroup, c.who, c.tool, c.detail, c.fields];
  });
  if (values.length) {
    sheet.getRange(sheet.getLastRow() + 1, 1, values.length, values[0].length).setValues(values);
  }
}

function buildHtml(account, changes, days, auto) {
  let html = '<div style="font-family:Arial,sans-serif;font-size:13px">'
    + '<p>Compte <b>' + escapeHtml(account.getName()) + '</b> (' + account.getCustomerId() + ') : '
    + changes.length + ' changement(s) de statut ou de budget sur les ' + days + ' derniers jours.</p>';
  if (auto) {
    html += '<p style="color:#b32d2e"><b>' + auto + ' changement(s) ont été appliqués par une règle ou une recommandation automatique.</b> '
      + 'Vérifiez qu\'ils correspondent à une décision de votre part.</p>';
  }
  if (changes.length) {
    html += '<table border="1" cellpadding="5" style="border-collapse:collapse;font-size:12px">'
      + '<tr><th align="left">Quand</th><th align="left">Type</th><th align="left">Campagne</th><th align="left">Qui</th><th align="left">Outil</th><th align="left">Détail</th></tr>';
    changes.slice(0, 200).forEach(function (c) {
      html += '<tr><td>' + escapeHtml(c.when) + '</td><td>' + escapeHtml(c.type) + '</td><td>' + escapeHtml(c.campaign)
        + (c.adGroup ? '<br><span style="color:#888">' + escapeHtml(c.adGroup) + '</span>' : '') + '</td><td>' + escapeHtml(c.who)
        + '</td><td>' + escapeHtml(c.tool) + '</td><td>' + escapeHtml(c.detail) + '</td></tr>';
    });
    html += '</table>';
    if (changes.length > 200) html += '<p>' + (changes.length - 200) + ' autres changements dans le Google Sheet.</p>';
  }
  html += '<p style="color:#888;font-size:12px">Script "Rapport des changements" par Pierre Bordier, pierrebordier.com. Lecture seule.</p></div>';
  return html;
}

function escapeHtml(s) {
  return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

Limites connues

  • L'historique ne remonte qu'à 30 jours et Google limite chaque lecture à 10 000 lignes : sur un compte très actif, préférez une exécution quotidienne.
  • Les noms de champs de l'historique évoluent parfois côté Google ; le script signale l'erreur au lieu de planter silencieusement.