/**
*
* Google Ads Script - Suggestions de mots-clés négatifs (version GRATUITE)
*
* (C) Nils Rooijmans , https://nilsrooijmans.com
*
* Ce script suggère des mots-clés négatifs pour les termes de recherche qui ont eu peu ou pas de conversions.
* Si de nouveaux négatifs sont suggérés, les négatifs sont signalés par e-mail. 
* L’e-mail contient un lien vers la feuille de calcul Google Doc documentant toutes les suggestions de mots-clés négatifs.
*
*INSTRUCTIONS:
* 
* Si vous débutez avec les scripts > suivez ces instructions pour savoir comment copier-coller et installer des scripts Google Ads :
* https://nilsrooijmans.com/how-to-set-up-and-run-google-ads-scripts/
* 
*Prochain:
* 1. Créez une nouvelle feuille Google (astuce pour les utilisateurs de Chrome : tapez simplement « sheets.new » dans la barre d'adresse)
* 2. Ajoutez l’URL complète de la feuille de calcul à l’SPREADSHEET_URL ci-dessous (ligne 35)
* 3. Décidez de la plage de dates / PÉRIODE (ligne 37)
* 4. Établir des seuils pour MIN_CONVERSIONS, MIN_CLICKS MIN_COST (lignes 42 à 44)
* 5. Ajoutez le nom de votre compte Google Ads (ligne 62)
* 6. Ajoutez votre adresse e-mail au script (ligne 65)
* 7. Autoriser et prévisualiser 
* 8. Horaire hebdomadaire (je préfère le lundi à 6 heures)
*
*
* Version : 1.2 - Ajout de la prise en charge des campagnes Shopping
* Version : 1.1 - Correction de bugs avec PERIOD et correction de la vérification pour voir si neg KW a été ajouté avec succès
* Version : 1.0
*
* ---------------------------------------------------
**/

DÉBUT DE LA CONFIGURATION DU SCRIPT ///

var SPREADSHEET_URL = «  » ; insérer l’URL de la nouvelle feuille de calcul Google
  
var PERIOD = « LAST_90_DAYS » ; La plage de dates de vos données de terme de recherche peut être « LAST_7_DAYS », « LAST_30_DAYS », « LAST_60_DAYS », « LAST_90_DAYS », « LAST_12_MONTHS » ou « THIS_YEAR »
  
/***SEUILS***/
  
var MIN_CONVERSIONS = 0,5 ; suggérer des mots-clés négatifs pour les termes de recherche qui ont eu moins de conversions au cours de la PÉRIODE que vous avez définie ci-dessus
  var MIN_CLICKS = 60 ; Ne regardez que les termes de recherche qui ont eu plus de ce nombre de clics au cours de la PÉRIODE que vous avez définie ci-dessus
  var MIN_COST = 5 ; Ne regardez que les termes de recherche qui ont dépassé ce montant de coût au cours de la PÉRIODE que vous avez définie ci-dessus
  
/*** FILTRES DE CAMPAGNE ***/
  
var CAMPAIGN_NAME_CONTAINS = «  » ;
    Utilisez-le si vous ne souhaitez traiter que certaines campagnes
    telles que les campagnes dont le nom contient « Marque » ou « Shopping ».
    Laissez «  » si vous souhaitez traiter toutes les campagnes.
  
var CAMPAIGN_NAME_DOES_NOT_CONTAIN = «  » ;
    Utilisez cette option si vous souhaitez exclure certaines campagnes
    telles que les campagnes dont le nom contient « Marque » ou « Shopping ».
    Laisser comme «  » si vous ne le souhaitez pas.
  
/*** PARAMÈTRES DE CONFIGURATION DE L’E-MAIL ***/
  var ACCOUNT_NAME = «  » ; insérer le nom abrégé pour reconnaître le compte
  var DEVISE = « USD » ; USD, EUR, etc.
  var SEND_EMAIL = vrai ;  
  var EMAIL_ADDRESSES = «  » ; Insérez les adresses e-mail des personnes qui souhaitent recevoir les alertes entre les guillemets, séparez les adresses e-mail multipe par une virgule

FIN DE LA CONFIGURATION DU SCRIPT ///

/*** NE CHANGEZ RIEN EN DESSOUS DE CETTE LIGNE ***/

var DEBUG = vrai ;

var EMAIL_SUBJECT = ACCOUNT_NAME+ » - Nouvelles suggestions de mots-clés négatifs basées sur des termes de recherche peu performants « ;
var EMAIL_BODY = « Ce script examine tous les groupes d’annonces activés dans toutes les campagnes activées, et"+
                   « suggère des mots-clés à exclure au niveau du groupe d’annonces pour les termes de recherche ayant généré plus de « +MIN_CLICKS+ » clics"+
                   « ET dépensez plus de « +MIN_COST+ » « +DEVISE+ » ET moins de conversions « +MIN_CONVERSIONS+ » pendant « +PERIOD+ ».\n » ;

fonction main() {
  
var result = getResults() ;
  reportResults(résultat) ;  
}

fonction reportResults(result) {
  
var outputSheet = prepareOutputSheets() ;
  var nrOfResultsReported = 0 ; 

var output = result.output ;

if (output.length > 0) {
    addOutputToSheet(sortie, sortieSheet) ;
    nrOfResultsReported += output.length ;
  }      

if(SEND_EMAIL &&& nrOfResultsReported > 0) {
    var emailSubject = « [Gads Script] « +EMAIL_SUBJECT ;     
    var emailFooter = getEmailFooter() ; 
    var emailBody = EMAIL_BODY+"\nNombre de suggestions de mots-clés négatifs ajoutées au total : « +nrOfResultsReported+"\n\n"+
                    « Vous pouvez trouver toutes les suggestions de mots-clés négatifs ici : « +SPREADSHEET_URL+"\n\n"+ 
                    « ---\n"+emailPied de page ;
    var emailQuotaRemaining = MailApp.getRemainingDailyQuota() ;
    Logger.log(« Quota d’e-mails restant : " + emailQuotaRemaining) ;
    MailApp.sendEmail(EMAIL_ADDRESSES, emailSubject, emailBody) ;
  }
}

function getResults() {

var accountName = AdsApp.currentAccount().getName() ;
  var results = [] ;
  var date = getTodaysDate() ;
  Logger.log(« date : « +date) ;
  
var startDate = getStartDate(PERIOD) ;
  var endDate = getEndDate(PERIOD) ;
  
var oùClause =       
      « OÙ metrics.clicks > " + MIN_CLICKS +
      « ET metrics.cost_micros > " + versArgent(MIN_COST) +
      « AND metrics.conversions < " + MIN_CONVERSIONS ;
  
si (CAMPAIGN_NAME_CONTAINS != «  » ) {
    whereClause = whereClause +
      « ET campaign.name COMME ' % » + CAMPAIGN_NAME_CONTAINS + « %' » ;
  }
  
if (CAMPAIGN_NAME_DOES_NOT_CONTAIN != «  » ) {
    whereClause = whereClause +
      « ET campaign.name PAS COMME ' % » + CAMPAIGN_NAME_DOES_NOT_CONTAIN + « %' » ;
  }
  
whereClause = whereClause +
      « AND campaign.status = ACTIVÉ ET ad_group.status = ACTIVÉ » +
      « ET segments.date BETWEEN ' » + startDate + « ' AND ' » + endDate +"' » ;
      « TRIER PAR metrics.clicks DESC » ;
      + « LIMITE 10 » ;

var requête = 
      « SELECT search_term_view.search_term, campaign.name, campaign.advertising_channel_type, ad_group.name, ad_group.id, metrics.impressions, metrics.clicks, metrics.average_cpc, metrics.cost_micros, metrics.conversions » +
      « DE search_term_view » + whereClause ;
  
debug(requête) ;
  
essayez {
    var result = AdsApp.search(requête) ;

while (result.hasNext()) {

var ligne = résultat.next() ;
      debug(ligne) ;

var searchTerm = row.searchTermView.searchTerm ;
      var campaignName = row.campaign.name ;
      var campaignType = row.campaign.advertisingChannelType ;
      var adGroupName = row.adGroup.name ;
      var adGroupId = row.adGroup.id ;
      var impressions = row.metrics.impressions ;
      var clics = row.metrics.clicks ;
      var ctr = parseFloat((clics/impressions).toFixed(2)) ;
      var avgCPC = convertMoney(row.metrics.averageCpc).toFixed(2) ;
      var cost = convertMoney(row.metrics.costMicros).toFixed(2) ;
      var conversions = parseFloat((row.metrics.conversions).toFixed(2)) ;
      var conversionRate = parseFloat((conversions/clics).toFixed(2)) ;
      var costPerConversion = parseFloat((coût/conversions).toFixed(2)) ;

Logger.log(« \nCandidat potentiel au mot-clé négatif : « +searchTerm+ » pour le groupe d'annonces : « +adGroupName+ » (adGroupId :"+adGroupId+ ») dans la campagne : '"+campaignName+"' du type de campagne : « +campaignType) ;

var isShoppingCampaign = (campaignType == 'SHOPPING') ;
      debug (« isShoppingCampaign : « +isShoppingCampaign) ;

if( !isNegated(searchTerm, adGroupId, isShoppingCampaign)) {
        console.log('Suggestion de mot-clé à exclure : campagne '${campaignName}' | adgroup '${adGroupName}' | searchTerm '${searchTerm}' | clics ${clics} | conversions ${conversions}') ;

results.push([date, accountName, campaignName, adGroupName, impressions, clics, ctr, avgCPC, cost, conversions, conversionRate, costPerConversion, searchTerm, « Terme de recherche non encore annulé"]) ;
      }   
    }
  } attraper(e) {
    alert(« ERROR : « +e) ;
    MailApp.sendEmail(EMAIL_ADDRESSES, « ### ERREUR : « +EMAIL_SUBJECT, « Impossible de suggérer des mots-clés négatifs : « +e) ;
  }

Logger.log(« \nAnalyses terminées\nNombre de nouvelles suggestions de mots-clés négatifs : « +results.length) ;
  
retour { 
    Extrant : Résultats
  };
}

function isNegated(mot-clé, adGroupId, isShoppingCampaign) {
  
var negativeKWs = getNegativeKeywords(adGroupId, isShoppingCampaign) ;
  
for(var i=0 ; i<négatifKWs.longueur ; i++) {
    if(negativeBlocksPositive(negativeKWs[i], mot-clé)) {
      Logger.log(« Mot-clé déjà inversé : « +mot-clé) ;
      return true ;
    }
  }
    
return false ;
}

function getNegativeKeywords(adGroupId, isShoppingCampaign) {

var ids = [adGroupId] ;
  var negKWs = [] ;
  var campaignId ;
  
Tout d'abord, nous allons récupérer tous les mots-clés à exclure du groupe d'annonces
  if ( !isShoppingCampaign) {
    var adGroupIterator = adGroupIterator = AdsApp.adGroups().withIds(ids).get() ;
  } sinon {
    var adGroupIterator = adGroupIterator = AdsApp.shoppingAdGroups().withIds(ids).get() ;
  }
  
while (adGroupIterator.hasNext()) {
    var adGroup = adGroupIterator.next() ;
    var negKWIterator = adGroup.negativeKeywords().get() ;
    
while (negKWIterator.hasNext()) {
      var negativeKW = negKWIterator.next() ;
      negKWs.push(négatifKW) ;
    }
    
campaignId = adGroup.getCampaign().getId() ;
  }
  
Deuxièmement : Récupérons tous les mots-clés négatifs de la campagne
  var campaignIds = [campaignId] ;
  
if ( !isShoppingCampaign) {
    var campaignIterator = AdsApp.campaigns().withIds(campaignIds).get() ;
  } sinon {
    var campaignIterator = AdsApp.shoppingCampaigns().withIds(campaignIds).get() ;
  }
  
while (campaignIterator.hasNext()) {
    var campagne = campaignIterator.next() ;
    
Récupérons tous les mots-clés négatifs au niveau de la campagne
    var negKWIterator = campaign.negativeKeywords().get() ;
    
while (negKWIterator.hasNext()) {
      var negativeKW = negKWIterator.next() ;
      negKWs.push(négatifKW) ;
    }
    
Récupérons tous les mots-clés négatifs des listes de mots-clés négatifs associés à cette campagne
    var negativeKeywordListIterator = campaign.negativeKeywordLists().withCondition('Status = ACTIVE').get() ;
    
while (negativeKeywordListIterator.hasNext()) {
      var negativeKeywordList = negativeKeywordListIterator.next() ;
      var negativeKeywordIterator = negativeKeywordList.negativeKeywords().get() ;
      
while (negativeKeywordIterator.hasNext()) {
        var negativeKW = negativeKeywordIterator.next() ;
        negKWs.push(négatifKW) ;
      }
    }
  }
  
retour des negKW ;
}

function negativeBlocksPositive(mot-clé négatif, mot-clé positifTexte) {
  
switch (negativeKeyword.getMatchType()) {
    cas « BROAD » :
      return hasAllTokens(negativeKeyword.getText(), positiveKeywordText) ;
      casser;

cas 'PHRASE' :
      return isSubsequence(negativeKeyword.getText(), positiveKeywordText) ;
      casser;

cas 'EXACT' :
      return positiveKeywordText === negativeKeyword.getText().replace('[', '').replace(']', '') ;
      casser;
  }
}

/**
 * Teste si tous les jetons du texte brut d'un mot-clé apparaissent dans
 * les jetons du texte d'un deuxième mot-clé.
 *
 * @param {string} keywordText1 le texte brut du mot-clé dont les jetons peuvent
 * apparaissent dans le texte de l’autre mot-clé.
 * @param {string} keywordText2 le texte brut du mot-clé qui peut contenir
 * les jetons de l’autre mot-clé.
 * @return {boolean} Indique si tous les jetons dans keywordText1 apparaissent parmi
 * les jetons de keywordText2.
 */
function hasAllTokens(mot-cléTexte1, mot-cléTexte2) {
  var mot-cléJetons1 = mot-cléTexte1.split(' ') ;
  var mot-cléJetons2 = mot-cléTexte2.split(' ') ;

for (var i = 0 ; i < keywordTokens1.length ; i++) {
    if (keywordTokens2.indexOf(keywordTokens1[i]) == -1) {
      return false ;
    }
  }

return true ;
}

/**
 * Teste si tous les jetons du texte brut d'un mot-clé apparaissent dans
 * Ordre dans les jetons du texte d'un deuxième mot-clé.
 *
 * @param {string} keywordText1 le texte brut du mot-clé dont les jetons peuvent
 * apparaissent dans le texte de l’autre mot-clé.
 * @param {string} keywordText2 le texte brut du mot-clé qui peut contenir
 * les jetons de l’autre mot-clé dans l’ordre.
 * @return {boolean} Indique si tous les jetons dans keywordText1 apparaissent dans l’ordre
 * parmi les jetons de keywordText2.
 */
function isSubsequence(mot-cléTexte1, mot-cléTexte2) {
  return (' ' + mot-cléTexte2 + ' ' ).indexOf(' ' + mot-cléTexte1 + ' ') >= 0 ;
}

/*** FONCTIONS DE RAPPORT ***/

function prepareOutputSheets() {
  
var tableur = SpreadsheetApp.openByUrl(SPREADSHEET_URL) ;
  if ( !tableur) {
    Logger.log (« Impossible d’ouvrir une nouvelle feuille de calcul de reporting ») ;
    rendre;
  }

var sheet = spreadsheet.getActiveSheet() ;
  if ( !sheet) {
    Logger.log (« Impossible d’ouvrir une nouvelle feuille de rapport ») ;
    rendre;
  }  
  
var numberOfRows=sheet.getLastRow() ;
  debug(« NR de lignes dans la feuille de sortie : « +numberOfRows) ;
  
Définir la largeur des colonnes
  sheet.setColumnWidth(1, 80) ;
  sheet.setColumnWidth(2, 200) ;
  sheet.setColumnWidth(3, 300) ;
  sheet.setColumnWidth(4, 200) ;
  sheet.setColumnWidth(5, 100) ;
  sheet.setColumnWidth(6, 60) ;
  sheet.setColumnWidth(7, 60) ;
  sheet.setColumnWidth(8, 60) ;  
  sheet.setColumnWidth(9, 60) ;
  sheet.setColumnWidth(10, 80) ;
  sheet.setColumnWidth(11, 80) ;    
  sheet.setColumnWidth(12, 80) ;
  sheet.setColumnWidth(13, 200) ;
  sheet.setColumnWidth(14, 100) ;
 
addHeaderToOutputSheet(sheet) ;

feuille de retour ;
}

function addHeaderToOutputSheet(sheet) {
 
essayez {
    var headerSheet = SpreadsheetApp.openByUrl(« https://docs.google.com/spreadsheets/d/11BzTJ4MhjgVrJ7gN9TpZiVFj89aRRBEklABT4onQiOo/ »).getSheetByName('header_sheet') ;
  } attraper(e) {
    console.log('### Il y a eu un problème lors de l’ouverture de la feuille d’en-tête. Veuillez télécharger la dernière version de ce script à l’adresse https://nilsrooijmans.com\n${e}') ;
    throw '### Il y a eu un problème lors de l’ouverture de la feuille d’en-tête. Veuillez télécharger la dernière version de ce script à l’adresse https://nilsrooijmans.com\n${e}' ;
  }
   
var headerRange = headerSheet.getRange(1, 1, 2, headerSheet.getLastColumn()) ;
  var headerData = headerRange.getValues() ;
   
console.log(« Ajout d’un en-tête à la feuille de sortie ») ; 
 
var range=sheet.getRange(1,1,2,headerData[1].length) ;
  range.clear() ;
  range.clearFormat() ;
  range.setValues(headerData)
  range.setFontWeight(« gras ») ;
  range = sheet.getRange(1,1,1,headerData[1].length) ;
  range.setFontColor('#007BFF')
  sheet.setFrozenRows(2) ;
   
}

function addOutputToSheet(sortie, feuille) {

var numberOfRows=sheet.getLastRow() ;
  
sheet.insertRowsBefore(3, output.length) ;

var startRow = 3 ;
  
var range=sheet.getRange(startRow, 1, output.length, output[0].length) ;
  range.setValues(sortie) ;
  range.sort([{colonne : 1, croissant : faux}, {colonne : 5, croissant : faux}]) ;    

}

function getEmailFooter() {
  
essayez {
    var footerSheet = SpreadsheetApp.openByUrl(« https://docs.google.com/spreadsheets/d/11BzTJ4MhjgVrJ7gN9TpZiVFj89aRRBEklABT4onQiOo/ »).getSheetByName('email_footer') ;
  } attraper(e) {
    console.log('### Il y a eu un problème lors de l’ouverture de la feuille de email_footer. Veuillez télécharger la dernière version de ce script à l’adresse https://nilsrooijmans.com\n${e}') ;
    throw '### Il y a eu un problème lors de l’ouverture de la feuille de email_footer. Veuillez télécharger la dernière version de ce script à l’adresse https://nilsrooijmans.com\n${e}' ;
  }
  
var footerRange = footerSheet.getRange(1, 1, footerSheet.getLastRow(), 1) ;
  var footerData = footerRange.getValues() ;
     
console.log('Obtention du texte du pied de page d’un e-mail') ;
  
var footer = footerData.join('\n') ;
  
retour du pied de page ;
}

/*** FONCTIONS D’ASSISTANCE ***/

function debug(chaîne) {
  if (DEBUG == true) {
    Logger.log(« ** « +chaîne) ;
  }
}

fonction alert(chaîne) {
  Logger.log(« ### « +chaîne) ;
}

Convertir la valeur du coût en type de données
fonction versArgent(m) {
  retour m * 1000000 ;
}

Convertir le type de données Money en valeur de coût
function convertMoney(m) {
  retour m / 1000000 ;
}

function isInArray(valeur, tableau) { 
  return array.indexOf(valeur) > -1 ;
}

function getTodaysDate() {
  return new Date() ;
}

function getStartDate(period) {
  
var startDate ;
  var today = nouveau Date() ;
  
switch(point) {
      
Cas « THIS_YEAR » : 
      startDate = today.getFullYear()+ »-01-01 » ;
      casser;

Cas « LAST_7_DAYS » :
      startDate = dateToISOString(new Date(Date.now() - 7*864e5)) ; 864E5 == 86400000 == 24*60*60*1000 
      casser;

Cas « LAST_30_DAYS » :
      startDate = dateToISOString(new Date(Date.now() - 30*864e5)) ; 864E5 == 86400000 == 24*60*60*1000 
      casser;

Cas « LAST_60_DAYS » :
      startDate = dateToISOString(new Date(Date.now() - 60*864e5)) ; 
      casser;

Cas « LAST_90_DAYS » :
      startDate = dateToISOString(new Date(Date.now() - 90*864e5)) ; 
      casser;

Cas « LAST_12_MONTHS » :
      startDate = dateToISOString(new Date(Date.now() - 365*864e5)) ; 
      casser;
      
faire défaut:
      startDate ="2021-01-01 » ;
  }
  
debug(« startDate : « +startDate) ;
  
return startDate ;
}

function getEndDate(period) {

var endDate ;
  
var yesterday = new Date(Date.now() - 864e5) ; 864E5 == 86400000 == 24*60*60*1000 
 
switch(point) {
      
Cas « LAST_YEAR » : 
      endDate = « dernierJourDernièreAnnée » ; À FAIRE
      casser;
      
Cas « LAST_MONTH » :
      endDate = « lastDayLastMonth » //TODO
      casser;
      
faire défaut:
      endDate = dateToISOString(hier) ;
  }

debug(« endDate : « +endDate) ;
  
return endDate ;
}

function dateToISOString(date) {
   
var année = date.getUTCFullYear() ;
  var mois = date.getUTCMonth()+1 ;
  si (mois < 10) {
    mois = « 0"+mois ;
  }
    
var jour = date.getUTCDate() ;
  si (jour < 10) {
    jour = « 0"+jour ;
  }  
  var ISOString = année+ »-« +mois+ »-« +jour ;
  
retour ISOString ;
}