Showing posts with label account cleanup. Show all posts
Showing posts with label account cleanup. Show all posts

Wednesday, April 10, 2013

Report on Broken Urls In Your Account

Note: If you are looking for a version of this script to run at the MCC level, check out Monitor Broken Links Using MCC Level Scripts.

UPDATED: 2013-05-20: Based on a comment from a reader, the script now only checks active campaigns and adgroups and it also only checks each url once.

UPDATED: 2013-04-28: Based on a comment from a reader, I made some updates to this script which include adding the response code to the email and formatting the results as an attachment.

It happens to the best of us. Sometimes, we remove pages on our site or update links and forget to make the corresponding change our SEM accounts. So tonight I put together a quick script to run through all your ads and keywords and create an email report with any of them that return a 404 Not Found or a 500 Server Error response code. You can easily add more error codes to check for by adding them to the BAD_CODES array at the beginning of the script.

Thanks,
Russ

/****************************
* Find Broken Urls In Your Account
* Version 1.1
* ChangeLog v1.1
*  - Updated to only see Text Ads
* Created By: Russ Savage
* FreeAdWordsScripts.com
****************************/
function main() {
  // You can add more if you want: http://goo.gl/VhIX
  var BAD_CODES = [404,500];
  var TO = ['email@example.com'/*,'email_address_2@example.com'*/];
  var SUBJECT = 'Broken Url Report - ' + _getDateString();
  var HTTP_OPTIONS = {
    muteHttpExceptions:true
  };
   
  //Let's look at ads and keywords for urls
  var iters = [
    //For Ad Level Urls
    AdWordsApp.ads()
      .withCondition("Status = 'ENABLED'")
      .withCondition("AdGroupStatus = 'ENABLED'")
      .withCondition("CampaignStatus = 'ENABLED'")
      .withCondition("Type = 'TEXT_AD'")
      .get(),
    //For Keyword Level Urls
    AdWordsApp.keywords()
      .withCondition("Status = 'ENABLED'")
      .withCondition("DestinationUrl != ''")
      .withCondition("AdGroupStatus = 'ENABLED'")
      .withCondition("CampaignStatus = 'ENABLED'")
      .get()
    ];
  
  var already_checked = {}; 
  var bad_entities = [];
  for(var x in iters) {
    var iter = iters[x];
    while(iter.hasNext()) {
      var entity = iter.next();
      if(entity.getDestinationUrl() == null) { continue; }
      var url = entity.getDestinationUrl();
      if(url.indexOf('{') >= 0) {
        //Let's remove the value track parameters
        url = url.replace(/\{[0-9a-zA-Z]+\}/g,'');
      }
      if(already_checked[url]) { continue; }
      var response_code;
      try {
        Logger.log("Testing url: "+url);
        response_code = UrlFetchApp.fetch(url, HTTP_OPTIONS).getResponseCode();
      } catch(e) {
        //Something is wrong here, we should know about it.
        bad_entities.push({e : entity, code : -1});
      }
      if(BAD_CODES.indexOf(response_code) >= 0) {
        //This entity has an issue.  Save it for later. 
        bad_entities.push({e : entity, code : response_code});
      }
      already_checked[url] = true;
    }
  }
  var column_names = ['Type','CampaignName','AdGroupName','Id','Headline/KeywordText','ResponseCode','DestUrl'];
  var attachment = column_names.join(",")+"\n";
  for(var i in bad_entities) {
    attachment += _formatResults(bad_entities[i],",");
  }
  if(bad_entities.length > 0) {
    var options = { attachments: [Utilities.newBlob(attachment, 'text/csv', 'bad_urls_'+_getDateString()+'.csv')] };
    var email_body = "There are " + bad_entities.length + " urls that are broken. See attachment for details.";
     
    for(var i in TO) {
      MailApp.sendEmail(TO[i], SUBJECT, email_body, options);
    }
  }  
}
 
//Formats a row of results separated by SEP
function _formatResults(entity,SEP) {
  var e = entity.e;
  if(typeof(e['getHeadline']) != "undefined") {
    //this is an ad entity
    return ["Ad",
            e.getCampaign().getName(),
            e.getAdGroup().getName(),
            e.getId(),
            e.getHeadline(),
            entity.code,
            e.getDestinationUrl()
           ].join(SEP)+"\n";
  } else {
    // and this is a keyword
    return ["Keyword",
            e.getCampaign().getName(),
            e.getAdGroup().getName(),
            e.getId(),
            e.getText(),
            entity.code,
            e.getDestinationUrl()
           ].join(SEP)+"\n";
  }
}
 
//Helper function to format todays date
function _getDateString() {
  return Utilities.formatDate((new Date()), AdWordsApp.currentAccount().getTimeZone(), "yyyy-MM-dd");
}

Monday, March 4, 2013

Fixing Capitalization Errors in Your Ads

One of the issues that marketers face when they have systems for automatically building creatives from feeds is quality control. I recently ran into an issue where I created around 1,000 new ads where the city name was IN ALL CAPS. Google of course doesn't like that much and I didn't like the idea of having to manually update 1,000 ads, so I put together the following script.

The script runs through your account for all of the disapproved ads and tries to find ones with words in ALL CAPS. I'm not sure what Google's limit on caps is, but in the script below, it looks for anything with 3 or more capital letters in a row. Then it replaces them, creates a new ad, and if it was successful, deletes the old ad.

I ran into some trouble trying to identify the reasons the ads were disapproved. It is not yet available in the API, but it has been requested. The other issue was that sometimes, the new ad you create will fail, which will cause you to delete the old one and not create a new one. I solved that one by counting the ads in the adgroup before and after I created the new ad in order to make sure my ad had been created. Again, I posted the feature request in the adwords scripting forums.

Thanks,
Russ

//-----------------------------------
// Fix Ads with EXCESSIVE CAPITALIZATION
// Created By: Russ Savage
// FreeAdWordsScripts.com
//-----------------------------------
function main() {
  var find_caps = /[A-Z]{3,}/g;
  var SEP = '~~@~~'; // this needs to be something you would never put in your ads.
  var ad_iter = AdWordsApp.ads().withCondition("ApprovalStatus = 'DISAPPROVED'").get();
  
  while(ad_iter.hasNext()) {
    var ad = ad_iter.next();
    var old_ad_cnt = get_ad_count(ad.getAdGroup());
    var old_ad_str = [ad.getHeadline(),ad.getDescription1(),ad.getDescription2(),ad.getDisplayUrl()].join(SEP);
    var new_ad_str = old_ad_str;
    Logger.log("Before:"+old_ad_str);
    var m = "";
    while((m = find_caps.exec(new_ad_str)) != null) {
      new_ad_str = replace_all(new_ad_str,m[0],init_cap(m[0]),false);
    }
    Logger.log("After:"+new_ad_str);
    if(old_ad_str != new_ad_str) {
      var [new_headline,new_desc1,new_desc2,new_disp_url] = new_ad_str.split(SEP);
      ad.getAdGroup().createTextAd(new_headline, new_desc1, new_desc2, new_disp_url, ad.getDestinationUrl());
      var new_ad_cnt = get_ad_count(ad.getAdGroup());
      if(new_ad_cnt == (old_ad_cnt+1)) {
        ad.remove();
      }
    } else {
      Logger.log("Skipping because no changes were made."); 
    }
  }
  
  function init_cap(s) {
    return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
  }
  
  // This function was adapted from: http://dumpsite.com/forum/index.php?topic=4.msg8#msg8 
  function replace_all(original,str1, str2, ignore) {
    return original.replace(new RegExp(str1.replace(/([\/\,\!\\\^\$\{\}\[\]\(\)\.\*\+\?\|\<\>\-\&])/g,"\\$&"),(ignore?"gi":"g")),(typeof(str2)=="string")?str2.replace(/\$/g,"$$$$"):str2);
  }
  
  function get_ad_count(ad_group) {
    var ad_iter = ad_group.ads().get();
    var new_ad_cnt = 0;
    while(ad_iter.hasNext()) {
      ad_iter.next();
      new_ad_cnt++;
    }
    return new_ad_cnt;
  }
}

Tuesday, December 11, 2012

Auto Add ValueTrack Params To All Destination URLs

Today we have a script that will help make sure all of your keywords are tagged with the appropriate ValueTrack parameters.  You can use this to ensure that all of your tracking params are added correctly to your urls.

You can add your own query string parameters to the mapping at the top of the script.  For example, if all the keywords in your account need to have the parameter "channel=sem" in their destination url, you could add that to the URL_PARAMS_TO_ADD mapping at the top of the file and it will be added to all your urls.

Also, I updated the script to allow you to add whatever values you want from the account into the url. See the examples for adding Campaign and AdGroup names to the url if you like.

Thanks,
Russ

/******************************************
* Auto Add ValueTrack (and other) Params If Not There
* Created By: Russ Savage
* Version 1.1
* ChangeLog v1.1
*  - Added the ability to add function calls into the parameters to add
* FreeAdWordsScripts.com
******************************************/
function main() {
   
  var URL_PARAMS_TO_ADD = { 
    "kw" : "{keyword}",
    "crid" : "{creative}",
    "mt" : "{matchtype}",
    "ntwk" : "{network}",
    "ap" : "{adposition}",
    "dv" : "{device}",
    "dvm" : "{devicemodel}",
    //"camp" : getCampaignName,  <----- This function is defined below
    //"ag" : getAdGroupName  <----- and so is this one
  };
   
  var kw_iter = AdWordsApp.keywords()
                  .withCondition("DestinationUrl != ''")
                  .get();
  
  while(kw_iter.hasNext()) {
    var kw = kw_iter.next();
    var destUrl = kw.getDestinationUrl();
    if(!destUrl || destUrl === '') { continue; }
    var toAppend = [];
    for(var key in URL_PARAMS_TO_ADD) {
      if(destUrl.indexOf(key) >= 0) { continue; }
      var value = URL_PARAMS_TO_ADD[key];
      
      //check to see if we should call a function
      if(typeof(value) === 'function') {
        value = encodeURI(value(kw));
      }
      toAppend.push(key+"="+value);
    }
    if(destUrl.indexOf('?') >= 0) {
      kw.setDestinationUrl(destUrl + "&"+toAppend.join('&'));
    } else {
      kw.setDestinationUrl(destUrl + "?"+toAppend.join('&'));
    }
    Logger.log(kw.getDestinationUrl());
  }
}

// As long as the function call is passed a kw object, you can insert
// whatever value you want into the url
function getCampaignName(kw) {
  return kw.getCampaign().getName();
}

function getAdGroupName(kw) {
  return kw.getAdGroup().getName();
}

Monday, November 26, 2012

Update Ads for the New Year

This is a companion to one of my earlier posts about updating your keywords for the new year.  You can easily run through each ad you have and update them all for the new year using the script below.  It will also pause the old ad as well, making sure your staying relevant to your customers.  You should schedule this to run every year on January 1st if your advertising relies heavily on dated traffic.

Thanks,
Russ

//-----------------------------------
// Update Ads for 2012
// Created By: Russ Savage
// FreeAdWordsScripts.com
//-----------------------------------
function main() {
  var OLD_YEAR = "2011";
  var NEW_YEAR = "2012";
  //You probably shouldn't update destination urls unless you know what you are doing.
  var FIELDS_CONTAINING_YEAR = ["Headline","Description1",
                                "Description2","DisplayUrl"
                                /*,"DestinationUrl"*/
                               ];
  
  for(i in FIELDS_CONTAINING_YEAR) {
    var field_iter = AdWordsApp.ads()
        .withCondition(FIELDS_CONTAINING_YEAR[i] + " CONTAINS " + OLD_YEAR)
        .withCondition("Status = ENABLED")
        .get();
    
    _iterateThroughAds(field_iter);
  }

  //---------------
  // Private Helper Functions
  //---------------  
  function _iterateThroughAds(ad_iter) {
    while (ad_iter.hasNext()) {
      var ad = ad_iter.next();
      var ag = ad.getAdGroup();
      _createNewAdFromOldAd(ag,ad);
    }
  }
  
  function _createNewAdFromOldAd(adgroup, old_ad) {
    //get the updated ad texts replacing all the old years with the new years
    var new_headline = old_ad.getHeadline().replace(OLD_YEAR,NEW_YEAR);
    var new_desc1 = old_ad.getDescription1().replace(OLD_YEAR,NEW_YEAR);
    var new_desc2 = old_ad.getDescription2().replace(OLD_YEAR,NEW_YEAR);
    var new_display_url = old_ad.getDisplayUrl().replace(OLD_YEAR,NEW_YEAR);
    var new_dest_url = old_ad.getDestinationUrl();/*.replace(OLD_YEAR,NEW_YEAR);*/
    
    //now create the new ad and pause the old one.
    adgroup.createTextAd(new_headline,new_desc1,new_desc2,new_display_url,new_dest_url);
    old_ad.pause();
  }
}

Friday, November 23, 2012

Automatically Pause Ads with Low CTR

Recently, Brad over at CertifiedKnowledge.com published some tips about testing at scale.  One of the most common testing techniques he mentions is to create a ton of ads and let Google optimize the rotation for you.  The problem with this technique is that the losers are rarely deleted.  Well, using the script below, you can find the worst performing ads in all your adgroups and pause it (if there is at least one other ad in the adgroup).

Thanks,
Russ


//-----------------------------------
// Pause Ads with Low CTR
// Created By: Russ Savage
// FreeAdWordsScripts.com
//-----------------------------------
function main() {
  // Let's start by getting all of the adGroups that are active
  var ag_iter = AdWordsApp.adGroups()
  .withCondition("Status = ENABLED")
  .get();

  // Then we will go through each one
  while (ag_iter.hasNext()) {
    var ag = ag_iter.next();
    var ad_iter = ag.ads()
      .withCondition("Status = ENABLED")
      .forDateRange("ALL_TIME")
      .orderBy("Ctr DESC")
      .get();
    var ad_array = new Array();
    while(ad_iter.hasNext()) {
      ad_array.push(ad_iter.next());
    }
    if(ad_array.length > 1) {
      for(var i = 1; i < ad_array.length; i++) {
        ad_array[i].pause(); //or .remove(); to delete them 
      }
    }
  }
}

Thursday, November 22, 2012

Delete All Disapproved Ads in an Account

Again, another simple script that just focuses on account maintenance. Over time, large accounts might build up thousands of adgroups with ads that might be disapproved. Sometimes, it makes sense to remove those and then slowly build and deploy new creative (we will have a script to build new creative later). Hopefully this will work for you.

Thanks,
Russ
//-----------------------------------
// Delete Ads That Are Disapproved
// Created By: Russ Savage
// FreeAdWordsScripts.com
//-----------------------------------
function main() {
  // Let's start by getting all of the ad that are disapproved
  var ad_iter = AdWordsApp.ads()
  .withCondition("ApprovalStatus != APPROVED")
  .get();

  // Then we will go through each one
  while (ad_iter.hasNext()) {
    var ad = ad_iter.next();
    // now we delete the ad
    Logger.log("Deleteing ad: " + ad.getHeadline());
    ad.remove();
  }
}

Wednesday, November 21, 2012

Pause AdGroups With No Active Keywords

This is a quick script to pause all the AdGroups with no active keywords in them. Not sure if this is super useful, but for large accounts, it might help identify AdGroups you can get rid of.

Thanks,
Russ

/*********************************************
* Pause AdGroups With No Active Keywords
* Version 1.1
* Changelog v1.1
*   - Updated for speed and added comments 
* Created By: Russ Savage
* FreeAdWordsScripts.com
**********************************************/
function main() {
  // Let's start by getting all of the active AdGroups
  var agIter = AdWordsApp.adGroups()
    .withCondition('CampaignStatus = ENABLED')
    .withCondition('Status = ENABLED')
    .get();
 
  // It is faster to store them and process them all at once later
  var toPause = [];
  // Then we will go through each one
  while(agIter.hasNext()) {
    var ag = agIter.next();
    //get all the keywords that are enabled
    var kwIter = ag.keywords()
      .withCondition("Status = ENABLED")
      .get();
    
    //If .hasNext() is true, there is at least 1 kw in the AdGroup
    var hasKw = kwIter.hasNext(); 
    if(!hasKw) {
      toPause.push(ag);
    }
  }
  
  // Now we process them all at once to take advantage of batch processing
  for(var i in toPause) {
    toPause[i].pause();
  }
}

Tuesday, November 20, 2012

Update Your Keywords for the Holiday Season

The other day, RKGBlog had a great post about updating your keywords for the holiday season. One of the mentions was to update all the years in your Keywords to the current year. Here is a little script that will find all the Keywords with the previous year in them and create new Keywords in the same AdGroup with the current year.

Thanks,
Russ
/*********************************************
* Update Keywords for the New Year
* Version 1.1
* Changelog v1.1
*   - Updated for speed and added comments 
* Created By: Russ Savage
* FreeAdWordsScripts.com
**********************************************/
function main() {
  var sameDayLastYear = new Date();
  sameDayLastYear.setYear(sameDayLastYear.getYear()-1);
  var oldYearStr = sameDayLastYear.getYear().toString();
  var newYearStr = new Date().getYear().toString();
  
  Logger.log('Updating keywords with old year: '+oldYearStr+' to new year: '+newYearStr);
  
  // Let's start by getting all of the keywords
  var kwIter = AdWordsApp.keywords()
    .withCondition("Text CONTAINS " + oldYearStr)
    .withCondition("Status = ENABLED")
    .withCondition("AdGroupStatus = ENABLED")
    .withCondition("CampaignStatus = ENABLED")
    .get();
 
  // It is always better to store and batch process afterwards
  var toPause = [];
  var toCreate = [];
  while (kwIter.hasNext()) {
    var kw = kwIter.next();
    var ag = kw.getAdGroup();
    var oldText = kw.getText();
    var newText = oldText.replace(oldYearStr,newYearStr);
    // Save the info so that we can create them as a batch later
    toCreate.push({ ag: ag, text: newText, cpc:kw.getMaxCpc(), destUrl : kw.getDestinationUrl() });
    // Same with the ones we want to pause
    toPause.push(kw) 
  }
  // Now we create the new keywords all at once
  for(var i in toCreate) {
    var elem = toCreate[i];
    elem.ag.createKeyword(elem.text, elem.cpc, elem.destUrl);
  }
  // And pause the old ones all at once
  for(var i in toPause) {
    toPause[i].pause();
    //or toPause[i].remove(); to delete the old keyword
  }
}

Monday, November 19, 2012

Pause All Keywords With No Impressions

Let's start with a very simple script. This one will find all of the keywords in your account that has never had an impression, and pause (or delete if you see the comment below) that keyword so that it will not negatively impact your quality score. According to Google, the longer something sits in your account and stagnates, the greater the impact to your quality score. As a reader pointed out, the fourth bullet here seems to contradict this statement. This would be a great script to schedule every few months to make sure you are trimming all the dead weight from your accounts.

Thanks,
Russ
/*********************************************
* Pause Keywords With No Impressions All Time
* Version 1.1
* Changelog v1.1
*   - Updated for speed and added comments 
* Created By: Russ Savage
* FreeAdWordsScripts.com
**********************************************/
var TO_NOTIFY = "your_email@domain.com";
function main() {
  // Let's start by getting all of the keywords with no impressions
  var kwIter = AdWordsApp.keywords()
    .withCondition("Impressions = 0") // could be "Clicks = 0" also
    .forDateRange("ALL_TIME") // could use a specific date range like "20130101","20131231"
    .withCondition("Status = ENABLED")
    .withCondition("CampaignStatus = ENABLED")
    .withCondition("AdGroupStatus = ENABLED")
    .get();
 
  // It is much faster to store all the keywords you want to process
  // and then make the changes all at once. This takes advantage
  // of the batch processing behind the scenes.
  var toPause = [];
  while (kwIter.hasNext()) {
    var kw = kwIter.next();
    toPause.push(kw);
    // This is to make sure you see things during the preview
    // When you run it for real, you can remove this clause to
    // increase speed.
    if(AdWordsApp.getExecutionInfo().isPreview() &&
       AdWordsApp.getExecutionInfo().getRemainingTime() < 10) {
      break;
    }
  }
  
  // Now go through each one and pause them.
  for(var i in toPause) {
    toPause[i].pause();
    //Or you could use toPause[i].remove(); to delete the keyword altogether
  }
  
  // Sent an email to notify you of the changes
  MailApp.sendEmail(TO_NOTIFY, 
                    "AdWords Script Paused "+toPause.length+" Keywords.", 
                    "Your AdWords Script paused "+toPause.length+" keywords.");
}