Me contacter

Écrivez-moi directement

Localisation Arlon, province de Luxembourg, Belgique

Carnet de bord

Astuces & bouts de code

Comme dans toute ascension, on note ce qu'on apprend en chemin. Ici : des scripts, des astuces et des bouts de code glanés au fil des projets — pratiques, testés, sans jargon inutile.

Automatisation Astuce n°1

Facebook : inviter automatiquement les personnes qui ont aimé un post à aimer votre page

Quand une publication décolle, Facebook affiche un bouton « Inviter » à côté de chaque personne qui a aimé le post, pour l'inviter à aimer votre page — mais cliquer un par un sur des centaines de profils n'a rien d'amusant. Ce petit script clique ces boutons automatiquement pour vous.

Ceci agit directement sur l'interface de Facebook — utilisez-le avec modération et gardez en tête que cela reste en dehors des outils officiels proposés par la plateforme.

facebook-page-invite.js
var buttons;
buttons = document.getElementsByClassName('_42ft');
for (var i = 0; i < buttons.length; i++) {
    if (buttons[i].getAttribute('ajaxify') != null) {
        if (buttons[i].getAttribute('ajaxify').indexOf('invite') != -1) {
            buttons[i].click();
        }
    }
}

Comment l'utiliser :

  1. Ouvrez la publication Facebook contenant la liste des personnes à inviter.
  2. Copiez le code ci-dessus et collez-le dans la console du navigateur (ou dans la barre d'adresse, voir version bookmarklet ci-dessous).
  3. Validez — les invitations partent automatiquement, une par une.

Version « bookmarklet » à coller directement dans la barre d'adresse :

javascript:var buttons; buttons = document.getElementsByClassName('_42ft'); for (var i = 0; i < buttons.length; i++) { if(buttons[i].getAttribute('ajaxify') != null){ if(buttons[i].getAttribute('ajaxify').indexOf('invite') != -1){ buttons[i].click(); } } }
Code créatif Astuce n°2

Effet de survol interactif « hey. » — structure de départ

Une grille interactive qui réagit au survol — le genre d'effet qu'on croise sur les portfolios créatifs. Chaque cellule tourne, s'éclaircit et retrouve ses couleurs au passage du curseur, puis revient doucement à son état de veille. Ci-dessous, une vraie simulation du principe, suivie du code complet (structure inspirée du travail de Jhey Tompkins, @jh3yy).

Simulation — passez le curseur sur la grille

La simulation ci-dessus est une version simplifiée : le code de référence ci-dessous (fourni tel quel) inclut en plus un panneau de réglages (Tweakpane) et GSAP pour le rendre déplaçable — retirés ici pour rester léger, mais présents dans le code complet. Cliquez sur chaque bloc pour l'ouvrir.

HTML index.html
<main><h1 class="fluid">hey.</h1><div class="grid">...
<main>
  <h1 class="fluid">hey.</h1>
  <div class="grid"></div>
</main>
<a
  aria-label="Follow Joachim"
  class="bear-link"
  href="https://www.linkedin.com/in/joachim-thiry/"
  target="_blank"
  rel="noreferrer noopener"
>
  <svg
    class="w-9"
    viewBox="0 0 969 955"
    fill="none"
    xmlns="http://www.w3.org/2000/svg"
  >
    <circle cx="161.191" cy="320.191" r="133.191" stroke="currentColor" stroke-width="20"></circle>
    <circle cx="806.809" cy="320.191" r="133.191" stroke="currentColor" stroke-width="20"></circle>
    <circle cx="695.019" cy="587.733" r="31.4016" fill="currentColor"></circle>
    <circle cx="272.981" cy="587.733" r="31.4016" fill="currentColor"></circle>
    <path
      d="M564.388 712.083C564.388 743.994 526.035 779.911 483.372 779.911C440.709 779.911 402.356 743.994 402.356 712.083C402.356 680.173 440.709 664.353 483.372 664.353C526.035 664.353 564.388 680.173 564.388 712.083Z"
      fill="currentColor"
    ></path>
    <rect x="310.42" y="448.31" width="343.468" height="51.4986" fill="#FF1E1E"></rect>
    <path
      fill-rule="evenodd"
      clip-rule="evenodd"
      d="M745.643 288.24C815.368 344.185 854.539 432.623 854.539 511.741H614.938V454.652C614.938 433.113 597.477 415.652 575.938 415.652H388.37C366.831 415.652 349.37 433.113 349.37 454.652V511.741L110.949 511.741C110.949 432.623 150.12 344.185 219.845 288.24C289.57 232.295 384.138 200.865 482.744 200.865C581.35 200.865 675.918 232.295 745.643 288.24Z"
      fill="currentColor"
    ></path>
  </svg>
</a>
CSS style.css
@import url("https://unpkg.com/normalize.css") layer(normalize); @layer normalize, base, demo, mobile; ...
@import url("https://unpkg.com/normalize.css") layer(normalize);

@layer normalize, base, demo, mobile;

@layer mobile {
  .grid div[data-hover] {
    transition-property: opacity, rotate, filter;
    transition-duration: 0s;
    rotate: calc(var(--grade, 0) * 90deg);
    filter: grayscale(0) brightness(1.5);
    opacity: 1;
  }
}

@layer demo {
  h1 {
    --font-level: 5;
    translate: 12px;
    margin: 0;
    margin-bottom: 1rem;
    color: color-mix(in hsl, canvasText, canvas 25%);
  }
  .grid {
    touch-action: none;
    display: grid;
    grid-template: repeat(var(--rows, 10), 1fr) / repeat(var(--cols, 10), 1fr);
    font-size: 2rem;
    font-weight: 600;
    line-height: 1;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    cursor: none;

    div {
      touch-action: none;
      padding: 2px;
      opacity: calc(var(--opacity) * 0.5);
      aspect-ratio: 1;
      display: grid;
      place-items: center;
      transition-property: opacity, rotate, filter;
      transition-duration: 0.8s, 0.4s, 0.6s;
      transition-timing-function: ease-in, ease-out, ease-out;
      scale: 1.5;
      filter: grayscale(1);
      color: hsl(var(--hue) 80% 50%);

      @media (prefers-color-scheme: dark) {
        opacity: calc(var(--opacity) + 0.2);
      }
      @media (hover: hover) and (pointer: fine) {
        &:hover {
          transition-property: opacity, rotate, filter;
          transition-duration: 0s;
          rotate: calc(var(--grade, 0) * 90deg);
          filter: grayscale(0) brightness(1.5);
          opacity: 1;
        }
      }
    }
  }
  [data-theme="dark"] .grid div {
    opacity: calc(var(--opacity) + 0.2);
    @media (hover: hover) and (pointer: fine) {
      &:hover {
        opacity: 1;
      }
    }
  }
}

@layer base {
  :root {
    --font-size-min: 16;
    --font-size-max: 20;
    --font-ratio-min: 1.2;
    --font-ratio-max: 1.33;
    --font-width-min: 375;
    --font-width-max: 1500;
  }

  html {
    color-scheme: light dark;
  }

  [data-theme="light"] {
    color-scheme: light only;
  }

  [data-theme="dark"] {
    color-scheme: dark only;
  }

  :where(.fluid) {
    --fluid-min: calc(
      var(--font-size-min) * pow(var(--font-ratio-min), var(--font-level, 0))
    );
    --fluid-max: calc(
      var(--font-size-max) * pow(var(--font-ratio-max), var(--font-level, 0))
    );
    --fluid-preferred: calc(
      (var(--fluid-max) - var(--fluid-min)) /
        (var(--font-width-max) - var(--font-width-min))
    );
    --fluid-type: clamp(
      (var(--fluid-min) / 16) * 1rem,
      ((var(--fluid-min) / 16) * 1rem) -
        (((var(--fluid-preferred) * var(--font-width-min)) / 16) * 1rem) +
        (var(--fluid-preferred) * var(--variable-unit, 100vi)),
      (var(--fluid-max) / 16) * 1rem
    );
    font-size: var(--fluid-type);
  }

  *,
  *:after,
  *:before {
    box-sizing: border-box;
  }

  body {
    overflow-x: hidden;
    background: light-dark(#fff, #000);
    display: grid;
    place-items: center;
    min-height: 100vh;
    font-family: "SF Pro Text", "SF Pro Icons", "AOS Icons", "Helvetica Neue",
      Helvetica, Arial, sans-serif, system-ui;
  }

  body::before {
    --size: 45px;
    --line: color-mix(in hsl, canvasText, transparent 80%);
    content: "";
    height: 100vh;
    width: 100vw;
    position: fixed;
    background: linear-gradient(
          90deg,
          var(--line) 1px,
          transparent 1px var(--size)
        )
        calc(var(--size) * 0.36) 50% / var(--size) var(--size),
      linear-gradient(var(--line) 1px, transparent 1px var(--size)) 0%
        calc(var(--size) * 0.32) / var(--size) var(--size);
    -webkit-mask: linear-gradient(-20deg, transparent 50%, white);
    mask: linear-gradient(-20deg, transparent 50%, white);
    top: 0;
    transform-style: flat;
    pointer-events: none;
    z-index: -1;
  }

  .bear-link {
    color: canvasText;
    position: fixed;
    top: 1rem;
    left: 1rem;
    width: 48px;
    aspect-ratio: 1;
    display: grid;
    place-items: center;
    opacity: 0.8;
  }

  :where(.x-link, .bear-link):is(:hover, :focus-visible) {
    opacity: 1;
  }

  .bear-link svg {
    width: 75%;
  }

  /* Utilities */
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
  }
}

div.tp-dfwv {
  width: 256px;
  position: fixed;
}
JavaScript script.js
import gsap from "https://esm.sh/gsap@3.13.0"; import Draggable from "https://esm.sh/gsap@3.13.0/Draggable"; ...
import gsap from "https://esm.sh/gsap@3.13.0";
import Draggable from "https://esm.sh/gsap@3.13.0/Draggable";
import { Pane } from "https://esm.sh/tweakpane@4.0.4";
gsap.registerPlugin(Draggable);

const config = {
  theme: "system",
  cols: 20,
  rows: 10
};

const ctrl = new Pane({
  title: "config",
  expanded: true
});

const grid = document.querySelector(".grid");

const buildGrid = () => {
  grid.innerHTML = new Array(config.cols * config.rows)
    .fill(0)
    .map(
      (_, index) => `
    <div style="
      --grade: ${Math.floor(Math.random() * 12 - 6)};
      --opacity: ${Math.min(Math.random(), 0.2)};
      --hue: ${Math.floor(Math.random() * 30)};
    ">+</div>
  `
    )
    .join("");
  grid.style.setProperty("--cols", config.cols);
  grid.style.setProperty("--rows", config.rows);
};
buildGrid();

const update = () => {
  document.documentElement.dataset.theme = config.theme;
};

const sync = (event) => {
  if (
    !document.startViewTransition ||
    event.target.controller.view.labelElement.innerText !== "theme"
  )
    return update();
  document.startViewTransition(() => update());
};

ctrl
  .addBinding(config, "cols", { min: 5, max: 30, step: 1 })
  .on("change", buildGrid);
ctrl
  .addBinding(config, "rows", { min: 5, max: 30, step: 1 })
  .on("change", buildGrid);
ctrl.addBinding(config, "theme", {
  label: "theme",
  options: { system: "system", light: "light", dark: "dark" }
});
ctrl.on("change", sync);
update();

// make tweakpane panel draggable
const tweakClass = "div.tp-dfwv";
const d = Draggable.create(tweakClass, {
  type: "x,y",
  allowEventDefault: true,
  trigger: tweakClass + " button.tp-rotv_b"
});
document.querySelector(tweakClass).addEventListener("dblclick", () => {
  gsap.to(tweakClass, {
    x: `+=${d[0].x * -1}`,
    y: `+=${d[0].y * -1}`,
    onComplete: () => {
      gsap.set(tweakClass, { clearProps: "all" });
    }
  });
});

if (window.matchMedia("(hover: none) and (pointer: coarse)").matches) {
  grid.addEventListener(
    "pointermove",
    (event) => {
      document.querySelector("[data-hover]")?.removeAttribute("data-hover");
      document.elementFromPoint(event.x, event.y).dataset.hover = "true";
    },
    true
  );
  grid.addEventListener(
    "pointerleave",
    () => {
      document.querySelector("[data-hover]")?.removeAttribute("data-hover");
    },
    true
  );
}
Automatisation Astuce n°3

Google Ads : 13 scripts utiles pour automatiser la gestion de compte

Une sélection de scripts Google Ads Scripts (JavaScript) pour automatiser la surveillance et l'optimisation d'un compte : rapports automatiques, pauses conditionnelles, ajustements d'enchères, alertes par e-mail. À adapter à votre compte avant utilisation — chaque script part du principe que vous remplacez les valeurs entre majuscules (URL de feuille de calcul, adresse e-mail, nom de campagne...) par les vôtres.

Testez toujours un nouveau script en mode aperçu (« Preview ») dans l'interface Google Ads Scripts avant de l'exécuter réellement — certains modifient des enchères ou mettent en pause des éléments de votre compte.

JavaScript Cartes thermiques (Heatmap CTR/Conversions)

Exporte les performances heure par heure et jour par jour vers Google Sheets pour générer une vue en carte thermique.

function main() {...
heatmap.js
function main() {
  var SPREADSHEET_URL = "VOTRE_URL_GOOGLE_SHEETS";
  var sheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getActiveSheet();
  sheet.clear();
  sheet.appendRow(["DayOfWeek", "HourOfDay", "Impressions", "Clicks", "CTR", "Cost"]);

  var report = AdsApp.report(
    "SELECT DayOfWeek, HourOfDay, Impressions, Clicks, Ctr, Cost " +
    "FROM CAMPAIGN_PERFORMANCE_REPORT " +
    "DURING LAST_30_DAYS"
  );

  var rows = report.rows();
  while (rows.hasNext()) {
    var row = rows.next();
    sheet.appendRow([
      row['DayOfWeek'],
      row['HourOfDay'],
      row['Impressions'],
      row['Clicks'],
      row['Ctr'],
      row['Cost']
    ]);
  }
}
JavaScript Suivi du Quality Score (Historique QS)

Enregistre le Quality Score quotidien des mots-clés dans un Google Sheet pour analyser l'évolution dans le temps.

function main() {...
quality-score-history.js
function main() {
  var SPREADSHEET_URL = "VOTRE_URL_GOOGLE_SHEETS";
  var sheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getActiveSheet();
  var today = Utilities.formatDate(new Date(), AdsApp.currentAccount().getTimeZone(), "yyyy-MM-dd");

  var keywords = AdsApp.keywords()
    .withCondition("Status = ENABLED")
    .withCondition("CampaignStatus = ENABLED")
    .get();

  while (keywords.hasNext()) {
    var kw = keywords.next();
    var qs = kw.getQualityScore();
    if (qs !== null) {
      sheet.appendRow([today, kw.getCampaign().getName(), kw.getAdGroup().getName(), kw.getText(), qs]);
    }
  }
}
JavaScript Pause des impressions « 0 »

Met en pause automatiquement les mots-clés actifs qui n'ont généré aucune impression sur une période donnée (ex : 90 jours).

function main() {...
pause-zero-impressions.js
function main() {
  var keywords = AdsApp.keywords()
    .withCondition("Status = ENABLED")
    .withCondition("Impressions = 0")
    .forDateRange("LAST_90_DAYS")
    .get();

  while (keywords.hasNext()) {
    var kw = keywords.next();
    kw.pause();
    Logger.log("Mot-clé mis en pause (0 impression) : " + kw.getText());
  }
}
JavaScript Vérificateur d'URL cassées & alertes 404 par e-mail

Vérifie les URL de destination des annonces actives, détecte les erreurs (HTTP différent de 200) et alerte par e-mail uniquement si des erreurs existent.

function main() {...
broken-url-checker.js
function main() {
  var EMAIL = "votre-email@exemple.com";
  var badUrls = [];

  var ads = AdsApp.ads()
    .withCondition("Status = ENABLED")
    .withCondition("CampaignStatus = ENABLED")
    .get();

  while (ads.hasNext()) {
    var ad = ads.next();
    var url = ad.urls().getFinalUrl();
    if (url) {
      try {
        var response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
        if (response.getResponseCode() !== 200) {
          badUrls.push(url + " (Code: " + response.getResponseCode() + ") - Annonce ID: " + ad.getId());
        }
      } catch (e) {
        badUrls.push(url + " (Erreur réseau) - Annonce ID: " + ad.getId());
      }
    }
  }

  if (badUrls.length > 0) {
    MailApp.sendEmail(
      EMAIL,
      "Alerte Google Ads : URL cassées détectées",
      "Les URL suivantes ont renvoyé une erreur :\n\n" + badUrls.join("\n")
    );
  }
}
JavaScript Analyse N-Gram (mots-clés de n mots)

Agrège les requêtes de recherche par groupes de 1, 2 ou 3 mots pour identifier les termes à exclure ou isoler.

function main() {...
ngram-analysis.js
function main() {
  var SPREADSHEET_URL = "VOTRE_URL_GOOGLE_SHEETS";
  var sheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getActiveSheet();
  sheet.clear();
  sheet.appendRow(["N-Gram", "Impressions", "Clicks", "Cost", "Conversions"]);

  var report = AdsApp.report(
    "SELECT Query, Impressions, Clicks, Cost, Conversions " +
    "FROM SEARCH_QUERY_PERFORMANCE_REPORT " +
    "DURING LAST_30_DAYS"
  );

  var ngrams = {};
  var rows = report.rows();

  while (rows.hasNext()) {
    var row = rows.next();
    var words = row['Query'].toLowerCase().split(" ");

    // Extrait les unigrammes (1 mot)
    for (var i = 0; i < words.length; i++) {
      var gram = words[i];
      if (!ngrams[gram]) ngrams[gram] = { imp: 0, clicks: 0, cost: 0, conv: 0 };
      ngrams[gram].imp += parseInt(row['Impressions']);
      ngrams[gram].clicks += parseInt(row['Clicks']);
      ngrams[gram].cost += parseFloat(row['Cost']);
      ngrams[gram].conv += parseFloat(row['Conversions']);
    }
  }

  for (var gram in ngrams) {
    var data = ngrams[gram];
    sheet.appendRow([gram, data.imp, data.clicks, data.cost, data.conv]);
  }
}
JavaScript Enchères 24/7 (ajustement heure par heure)

Ajuste les enchères d'une campagne en fonction de l'heure courante (ex : +20 % à 19h, -30 % à 9h).

function main() {...
hourly-bid-adjustment.js
function main() {
  var CAMPAIGN_NAME = "Nom_De_Votre_Campagne";
  var currentHour = parseInt(Utilities.formatDate(new Date(), AdsApp.currentAccount().getTimeZone(), "HH"));

  // Exemples de multiplicateurs par heure (1.0 = 0%, 1.2 = +20%, 0.7 = -30%)
  var modifiers = {
    9: 0.70,   // 09:00 -> -30%
    19: 1.20   // 19:00 -> +20%
  };

  var targetModifier = modifiers[currentHour] || 1.0;

  var campaigns = AdsApp.campaigns()
    .withCondition("Name = '" + CAMPAIGN_NAME + "'")
    .get();

  while (campaigns.hasNext()) {
    var campaign = campaigns.next();
    // Requis : planification horaire active dans la campagne
    Logger.log("Ajustement configuré pour l'heure " + currentHour + " : x" + targetModifier);
  }
}
JavaScript Ajustement dynamique des enchères CPC

Augmente le CPC max des mots-clés performants (CPA bas) et le diminue pour les mots-clés coûteux.

function main() {...
dynamic-cpc-adjustment.js
function main() {
  var TARGET_CPA = 30; // CPA cible en EUR

  var keywords = AdsApp.keywords()
    .withCondition("Status = ENABLED")
    .withCondition("Conversions > 3")
    .forDateRange("LAST_30_DAYS")
    .get();

  while (keywords.hasNext()) {
    var kw = keywords.next();
    var stats = kw.getStatsFor("LAST_30_DAYS");
    var cpa = stats.getCost() / stats.getConversions();
    var currentCpc = kw.bidding().getCpc();

    if (!currentCpc) continue;

    if (cpa < TARGET_CPA * 0.8) {
      // Bon sous-CPA : augmente l'enchère de 10%
      kw.bidding().setCpc(currentCpc * 1.10);
      Logger.log("CPC augmenté pour : " + kw.getText());
    } else if (cpa > TARGET_CPA * 1.2) {
      // Sur-CPA : baisse l'enchère de 15%
      kw.bidding().setCpc(currentCpc * 0.85);
      Logger.log("CPC diminué pour : " + kw.getText());
    }
  }
}
JavaScript Rapport de performance du compte par e-mail

Envoie un bilan périodique récapitulant la dépense, le nombre de conversions, le CPA moyen et le pacing budgétaire.

function main() {...
account-performance-report.js
function main() {
  var EMAIL = "votre-email@exemple.com";
  var stats = AdsApp.currentAccount().getStatsFor("THIS_MONTH");

  var cost = stats.getCost();
  var conv = stats.getConversions();
  var cpa = conv > 0 ? (cost / conv).toFixed(2) : "N/A";

  var today = new Date().getDate();
  var dailySpend = (cost / today).toFixed(2);

  var body = "Rapport de performance - Mois en cours :\n\n" +
             "- Dépense totale : " + cost + " €\n" +
             "- Conversions : " + conv + "\n" +
             "- CPA moyen : " + cpa + " €\n" +
             "- Dépense quotidienne moyenne : " + dailySpend + " €/jour\n";

  MailApp.sendEmail(EMAIL, "Rapport Google Ads - Mise à jour du compte", body);
}
JavaScript Script Gagnant / Perdant (labeling automatique)

Analyse la performance globale des annonces et leur applique automatiquement une étiquette (Label) Gagnant ou Perdant.

function main() {...
winner-loser-labeling.js
function main() {
  // Crée les labels s'ils n'existent pas
  if (!AdsApp.labels().withCondition("Name = 'Gagnant'").get().hasNext()) AdsApp.createLabel("Gagnant");
  if (!AdsApp.labels().withCondition("Name = 'Perdant'").get().hasNext()) AdsApp.createLabel("Perdant");

  var ads = AdsApp.ads()
    .withCondition("Status = ENABLED")
    .withCondition("Impressions > 500")
    .forDateRange("LAST_30_DAYS")
    .get();

  while (ads.hasNext()) {
    var ad = ads.next();
    var ctr = ad.getStatsFor("LAST_30_DAYS").getCtr();

    if (ctr > 0.05) { // CTR > 5%
      ad.removeLabel("Perdant");
      ad.applyLabel("Gagnant");
    } else if (ctr < 0.015) { // CTR < 1.5%
      ad.removeLabel("Gagnant");
      ad.applyLabel("Perdant");
    }
  }
}
JavaScript Pause des annonces à faible CTR dans un groupe

Identifie les groupes avec plusieurs annonces et met en pause l'annonce qui affiche le moins bon CTR (si au moins une alternative reste active).

function main() {...
low-ctr-ad-pause.js
function main() {
  var adGroups = AdsApp.adGroups()
    .withCondition("Status = ENABLED")
    .get();

  while (adGroups.hasNext()) {
    var ag = adGroups.next();
    var ads = ag.ads()
      .withCondition("Status = ENABLED")
      .forDateRange("LAST_30_DAYS")
      .get();

    if (ads.totalNumEntities() > 1) {
      var adList = [];
      while (ads.hasNext()) {
        var ad = ads.next();
        adList.push({
          entity: ad,
          ctr: ad.getStatsFor("LAST_30_DAYS").getCtr()
        });
      }

      // Trie par CTR croissant
      adList.sort(function(a, b) { return a.ctr - b.ctr; });

      // Pause la pire annonce si elle a au moins 200 impressions
      var worstAd = adList[0];
      if (worstAd.entity.getStatsFor("LAST_30_DAYS").getImpressions() > 200) {
        worstAd.entity.pause();
        Logger.log("Annonce à faible CTR mise en pause dans : " + ag.getName());
      }
    }
  }
}
JavaScript Enchères dépendantes de la météo

Exemple de structure interrogeant une API météo (OpenWeatherMap) pour appliquer un ajustement selon la température locale.

function main() {...
weather-based-bidding.js
function main() {
  var API_KEY = "VOTRE_CLE_API_OPENWEATHER";
  var CITY = "Paris";
  var url = "https://api.openweathermap.org/data/2.5/weather?q=" + CITY + "&units=metric&appid=" + API_KEY;

  var response = UrlFetchApp.fetch(url);
  var json = JSON.parse(response.getContentText());
  var temp = json.main.temp;

  Logger.log("Température actuelle à " + CITY + " : " + temp + "°C");

  var campaign = AdsApp.campaigns()
    .withCondition("Name = 'Campagne_Saisonnière'")
    .get()
    .next();

  if (temp > 25) {
    // Vague de chaleur : booste le budget / enchères
    Logger.log("Météo chaude : ajustement positif appliqué.");
  }
}
JavaScript Suivi du CTR historique vers Google Sheets

Alimente une feuille de calcul avec l'historique de CTR de chaque annonce pour suivre l'évolution des performances SKAG.

function main() {...
ctr-history-tracker.js
function main() {
  var SPREADSHEET_URL = "VOTRE_URL_GOOGLE_SHEETS";
  var sheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getActiveSheet();
  var date = Utilities.formatDate(new Date(), AdsApp.currentAccount().getTimeZone(), "yyyy-MM-dd");

  var ads = AdsApp.ads()
    .withCondition("Status = ENABLED")
    .forDateRange("LAST_7_DAYS")
    .get();

  while (ads.hasNext()) {
    var ad = ads.next();
    var stats = ad.getStatsFor("LAST_7_DAYS");
    sheet.appendRow([
      date,
      ad.getCampaign().getName(),
      ad.getAdGroup().getName(),
      ad.getId(),
      stats.getCtr(),
      stats.getImpressions()
    ]);
  }
}
JavaScript Rapport des groupes d'annonces en déclin

Détecte les ad groups dont les conversions ou le CTR ont chuté d'une semaine sur l'autre et les exporte dans un spreadsheet.

function main() {...
declining-adgroups-report.js
function main() {
  var SPREADSHEET_URL = "VOTRE_URL_GOOGLE_SHEETS";
  var sheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getActiveSheet();
  sheet.clear();
  sheet.appendRow(["Campagne", "Groupe d'annonces", "Conversions S-1", "Conversions S-2", "Baisse (%)"]);

  var adGroups = AdsApp.adGroups()
    .withCondition("Status = ENABLED")
    .get();

  while (adGroups.hasNext()) {
    var ag = adGroups.next();

    // Période récente (derniers 7 jours)
    var recentStats = ag.getStatsFor("LAST_7_DAYS");
    var recentConv = recentStats.getConversions();

    // Période précédente (8 à 14 jours)
    var prevStats = ag.getStatsFor("LAST_14_DAYS");
    var prevConv = prevStats.getConversions() - recentConv;

    if (prevConv > 5 && recentConv < prevConv * 0.5) {
      var drop = (((prevConv - recentConv) / prevConv) * 100).toFixed(1);
      sheet.appendRow([
        ag.getCampaign().getName(),
        ag.getName(),
        recentConv,
        prevConv,
        drop + "%"
      ]);
    }
  }
}

Une question ? Demandez directement