Durante le nostre peregrinazioni sul web abbiamo scoperto che Google ha rilasciato uno script per automatizzare e controllare ricorsivamente gli URL degli annunci di una campagna Adwords. L’esecuzione di tale script permette quindi di controllare quali di questi restituiscono un errore 404 e, di conseguenza, fanno atterrare un possibile cliente su una pagina inesistente, sprecando il budget investito nella campagna di advertising.

Come funziona
Lo script controlla fino a 800 url al giorno, in modo da non superare la quota giornaliera di 20000 esecuzioni imposta per i servizi di Google.
Una volta eseguito rilascia, grazie al collegamento con un foglio di calcolo di Google Drive, il conto totale degli annunci che puntano ad un URL esistente e quelli che, invece, generano un errore 404. Verrà inoltre inviata una mail all’indirizzo da noi inserito nel foglio di calcolo contenente la lista degli annunci con URL errato, per semplificare il processo di correzione della campagna.
Cosa serve per eseguire lo script
Per eseguire lo script è necessario:
- Un account Adwords con almeno una campagna di advertising configurata, attiva o in pausa.
- Un account su Google Cloud per salvare una copia locale dello script basato su un foglio di calcolo.
Configurare lo script di Adwords
Lo script è di veloce configurazione e per effettuare la sua esecuzione è necessario:
- Salvare una copia del foglio di calcolo su cui si basa lo script sul proprio Google Drive.
- Modificare l’indirizzo email sulla copia dello script.
- Copiare e incollare il seguente script su Adwords Operazioni collettive >> script e modificare la variabile
SPREADSHEET_URL
inserendo come valore l’url del foglio di calcolo salvato nel nostro Google Drive (attenzione l'url sarà diverso da quello indicato sopra) - Pianificare (se necessario) l’esecuzione dello script (quotidianamente/settimanalmente etc.).
Personalizzare lo script
Lo script si può personalizzare, ottimizzando la sua esecuzione in funzione del lavoro che vogliamo eseguire.
In particolare possiamo interagire con alcune variabili per modificare la sua azione:
- Modificando il contenuto di
STATUSES_TO_CHECK
all’interno dello script possiamo controllare gli annunci attivi, in pausa o entrambi. - Se vogliamo diminuire il numero degli URL da controllare possiamo diminuire il valore di
MAX_URLS_TO_CHECK
. - Per modificare la label assegnata agli URL controllati all’interno dello script modifichiamo il valore della variabile
LABEL_NAME
. - Per evitare di controllare eventuali url assegnati alle singole keywords, all’interno del foglio di calcolo condiviso modifichiamo il valore di “Check keywords?” da Yes a No.
- Per evitare il controllo degli annunci modifichiamo il valore di “Check ads?” da Yes a No.
- Per non far controllare gli URL dei sitelinks modifichiamo il valore di “Check sitelinks?”, all’interno del foglio di calcolo condiviso, da Yes a No.
IL CODICE DELLO SCRIPT - click per espandere questa sezione
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @name Link Checker
*
* @overview The Link Checker script iterates through all the ads and keywords
* in your account and makes sure their URLs do not produce "Page not found"
* or other types of error responses. See
* https://developers.google.com/adwords/scripts/docs/solutions/link-checker#adwordsapp
* for more details.
*
* @author AdWords Scripts Team [adwords-scripts@googlegroups.com]
*
* @version 1.2
*/
/**
* The URL of the tracking spreadsheet. This should be a copy of
* https://goo.gl/bMrbW5
*/
var SPREADSHEET_URL = 'YOUR_SPREADSHEET_URL';
/**
* Status code for items to be checked.
*/
var STATUSES_TO_CHECK = ['ENABLED', 'PAUSED'];
/**
* Limit the number of ad and keyword URLs checked by this script per run.
*/
var MAX_URLS_TO_CHECK = 800;
var LABEL_NAME = 'link_checked';
var shelper = new SHelper();
var badUrls = 0;
var numUrlsChecked = 0;
function main() {
var isFirstRun = isFirstRunOfTheDay();
if (isFirstRun) {
dealWithFirstRunOfTheDay();
}
if (shelper.config.email.length == 0 &&
shelper.config.emailPreference != 'Never') {
Logger.log('WARNING: no email specified, proceeding...');
}
if (!shelper.config.checkAds && !shelper.config.checkKeywords) {
Logger.log('WARNING: requested no keywords and no ads checking. Exiting.');
return;
}
createLinkCheckerLabel();
var anythingChanged = shelper.config.checkKeywords &&
checkKeywordUrls(AdWordsApp.keywords(), isFirstRun);
anythingChanged = (shelper.config.checkAds &&
checkAdUrls(AdWordsApp.ads(), isFirstRun)) || anythingChanged;
if (anythingChanged) {
shelper.flush();
if (badUrls > 0 && shelper.config.email.length > 0 &&
shelper.config.emailPreference == 'As soon as an error is discovered') {
var bad = shelper.spreadsheet.getRangeByName('bad').getValue();
var good = shelper.spreadsheet.getRangeByName('good').getValue();
sendReportWithErrors(good, bad);
}
} else {
shelper.spreadsheet.getRangeByName('finished').setValue(
'All done for the day!');
}
Logger.log('Done');
}
function isFirstRunOfTheDay() {
var date = new Date();
var lastCheckDate = shelper.dataSheet.getRange(1, 3).getValue();
return lastCheckDate.length == 0 ||
date.getYear() != lastCheckDate.getYear() ||
date.getMonth() != lastCheckDate.getMonth() ||
date.getDay() != lastCheckDate.getDay();
}
function dealWithFirstRunOfTheDay() {
var date = new Date();
Logger.log('The script is running for the first time today...');
// kill the label.
var labels = AdWordsApp.labels().withCondition(
"Name='" + LABEL_NAME + "'").get();
if (labels.hasNext()) {
if (AdWordsApp.getExecutionInfo().isPreview()) {
Logger.log('WARNING: This script needs to remove a label named "%s" ' +
'to work correctly, but this action cannot be performed in ' +
'preview mode. Please run this script, or remove the label ' +
'manually if you wish to preview the script.', LABEL_NAME);
}
labels.next().remove();
}
// send out yesterday's report
if (shelper.config.email.length > 0 &&
(shelper.config.emailPreference == 'Once a day' ||
shelper.config.emailPreference == 'Once a day if there are errors')) {
var bad = shelper.spreadsheet.getRangeByName('bad').getValue();
var good = shelper.spreadsheet.getRangeByName('good').getValue();
if (shelper.config.emailPreference == 'Once a day') {
if (bad == 0) {
MailApp.sendEmail(shelper.config.email,
'AdWords Link Checker verified ' + good +
' URLs on account ' +
AdWordsApp.currentAccount().getCustomerId() +
', all looking good!', '');
} else {
sendReportWithErrors(good, bad);
}
} else if (shelper.config.emailPreference ==
'Once a day if there are errors' && bad > 0) {
sendReportWithErrors(good, bad);
}
}
// reset the spreadsheet
shelper.spreadsheet.getRangeByName('account_id_dashboard').setValue(
AdWordsApp.currentAccount().getCustomerId());
shelper.spreadsheet.getRangeByName('account_id_report').setValue(
AdWordsApp.currentAccount().getCustomerId());
shelper.spreadsheet.getRangeByName('date').setValue(date);
shelper.spreadsheet.getRangeByName('finished').setValue(
'Checking links...');
shelper.dataSheet.getRange(
4, 1, shelper.dataSheet.getMaxRows() - 3, 6).clear();
}
function sendReportWithErrors(good, bad) {
var emailBody = [];
emailBody.push('Summary for account ' +
AdWordsApp.currentAccount().getCustomerId() +
': ' + good + ' good URLs, ' + bad + ' bad ones\n');
emailBody.push('Full report available at ' + shelper.spreadsheet.getUrl() +
'\n');
shelper.reset();
var row = shelper.readRow();
while (row != null && emailBody.length < 200) {
if (row[1] >= 300) {
var entityType = row[4].length > 0 ? 'Keyword: ' : 'Ad: ';
var entityText = row[4].length > 0 ? row[4] : row[5];
emailBody.push('Campaign: ' + row[2] + ', Ad Group: ' + row[3] + ', ' +
entityType + entityText);
emailBody.push(row[0] + ' - ' + row[1] + ' response code.\n');
}
row = shelper.readRow();
}
if (emailBody.length >= 200) {
emailBody.push('Further URLs omitted. Check the report at ' +
shelper.spreadsheet.getUrl());
}
shelper.reset();
MailApp.sendEmail(shelper.config.email,
'AdWords Link Checker verified found ' + bad +
' bad URLs on account ' + AdWordsApp.currentAccount().getCustomerId() + '',
emailBody.join('\n'));
}
function checkAdUrls(selector, isFirstRun) {
var iterator = selector
.withCondition('CreativeFinalUrls STARTS_WITH_IGNORE_CASE "h"')
.withCondition('Status IN [' + STATUSES_TO_CHECK.join(',') + ']')
.withCondition('CampaignStatus IN [' + STATUSES_TO_CHECK.join(',') + ']')
.withCondition('AdGroupStatus IN [' + STATUSES_TO_CHECK.join(',') + ']')
.withCondition('LabelNames CONTAINS_NONE ["' + LABEL_NAME + '"]')
.get();
Logger.log('Checking %s ads...', iterator.totalNumEntities());
if (iterator.totalNumEntities() == 0 && isFirstRun) {
Logger.log('WARNING: The script is not checking any ad URLs. If this is ' +
'not expected, then ensure that you have enabled ads in your ' +
'account. Also check the logs for any earlier errors.');
}
return checkUrls(iterator);
}
function checkKeywordUrls(selector, isFirstRun) {
var iterator = selector
.withCondition('FinalUrls STARTS_WITH_IGNORE_CASE "h"')
.withCondition('Status IN [' + STATUSES_TO_CHECK.join(',') + ']')
.withCondition('CampaignStatus IN [' + STATUSES_TO_CHECK.join(',') + ']')
.withCondition('AdGroupStatus IN [' + STATUSES_TO_CHECK.join(',') + ']')
.withCondition('LabelNames CONTAINS_NONE ["' + LABEL_NAME + '"]')
.get();
Logger.log('Checking %s keywords...', iterator.totalNumEntities());
if (iterator.totalNumEntities() == 0 && isFirstRun) {
Logger.log('WARNING: The script is not checking any keyword URLs. This ' +
'may happen if none of your keywords have a final URL. If this not ' +
'expected, then ensure that you have enabled keywords in your ' +
'account. Also check the logs for any earlier errors.');
}
return checkUrls(iterator);
}
function checkUrls(iterator) {
if (!iterator.hasNext()) {
return false;
}
var urlMap = {};
while (iterator.hasNext()) {
var entity = iterator.next();
if (numUrlsChecked > MAX_URLS_TO_CHECK) {
Logger.log('Checked %s urls. Will resume in next run.', numUrlsChecked);
break;
}
var urls = [entity.urls().getFinalUrl(), entity.urls().getMobileFinalUrl()];
for (var i = 0; i < urls.length; i++) {
if (urls[i] == null) {
continue;
}
var lastUrl = encodeURI(urls[i]);
if (lastUrl in urlMap) {
continue;
}
urlMap[lastUrl] = true;
var now = new Date().getTime();
var responseCode = 0;
try {
numUrlsChecked++;
var response = UrlFetchApp.fetch(lastUrl, {muteHttpExceptions: true});
responseCode = response.getResponseCode();
} catch (e) {
// Something went wrong. Since this a script error, let's mark it as
// 500.
Logger.log('Could not fetch %s due to an internal error : "%s". ' +
'Marking this URL as failed, with an error code 500.', lastUrl, e);
responseCode = 500;
}
var then = new Date().getTime();
Utilities.sleep(then - now);
if (responseCode < 300) {
shelper.writeRow(lastUrl, responseCode);
} else {
badUrls++;
if (typeof(entity['getHeadline']) != 'undefined') {
var adText = entity.getType() == 'TEXT_AD' ?
entity.getHeadline() + '\n' + entity.getDescription1() + '\n' +
entity.getDescription2() : entity.getType();
shelper.writeRow(lastUrl, responseCode,
entity.getCampaign().getName(),
entity.getAdGroup().getName(),
null, adText);
} else {
shelper.writeRow(lastUrl, responseCode,
entity.getCampaign().getName(),
entity.getAdGroup().getName(),
entity.getText());
}
}
}
entity.applyLabel(LABEL_NAME);
}
return true;
}
function createLinkCheckerLabel() {
var labels = AdWordsApp.labels().withCondition(
"Name='" + LABEL_NAME + "'").get();
if (!labels.hasNext()) {
if (AdWordsApp.getExecutionInfo().isPreview()) {
Logger.log('WARNING: This script needs to create a label named "%s" to ' +
'work correctly, but this action cannot be performed in preview ' +
'mode. Please run this script, or create the label manually if you ' +
'wish to preview the script.', LABEL_NAME);
}
AdWordsApp.createLabel(LABEL_NAME,
"Managed by Link Checker, please don't modify!", '#60e020');
}
}
// Spreadsheet helper
function SHelper() {
this.MAX_ROWS = 20000;
this.BATCH_SIZE = 50;
Logger.log('Using spreadsheet - %s.', SPREADSHEET_URL);
this.spreadsheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL);
this.dataSheet = this.spreadsheet.getSheets()[1];
this.config = {
checkAds: this.spreadsheet.getRangeByName('check_ads').getValue() == 'Yes',
checkKeywords: this.spreadsheet.getRangeByName('check_keywords').
getValue() == 'Yes',
email: this.spreadsheet.getRangeByName('email_address').getValue(),
emailPreference: this.spreadsheet.getRangeByName('email_preference').
getValue()
};
this.globalRow = 4;
this.cells = null;
this.localRow = 0;
this.reset = function() {
this.globalRow = 4;
this.cells = null;
this.localRow = 0;
};
this.readRow = function() {
initCells(this);
if (this.localRow == this.cells.length) {
this.globalRow += this.cells.length;
if (this.globalRow >= this.dataSheet.getMaxRows()) {
return null;
}
this.cells = this.dataSheet.getRange(
this.globalRow, 2, this.BATCH_SIZE, 6).getValues();
this.localRow = 0;
}
if (this.cells[this.localRow][0].length > 0) {
return this.cells[this.localRow++];
} else {
return null;
}
};
this.writeRow = function() {
fetchCells(this);
for (var i = 0; i < arguments.length; i++) {
this.cells[this.localRow][i] = arguments[i];
}
};
this.flush = function() {
if (this.cells) {
this.dataSheet.getRange(this.globalRow, 2, this.cells.length, 6).
setValues(this.cells);
this.dataSheet.getRange(1, 1).copyFormatToRange(
this.dataSheet,
3,
3,
this.globalRow,
this.globalRow + this.cells.length);
}
};
function initCells(instance) {
if (instance.cells == null) {
instance.globalRow = 4;
instance.cells = instance.dataSheet.getRange(
instance.globalRow, 2, instance.BATCH_SIZE, 6).getValues();
instance.localRow = 0;
}
}
function fetchCells(instance) {
initCells(instance);
while (!findEmptyRow(instance) && instance.globalRow < instance.MAX_ROWS) {
if (instance.dataSheet.getMaxRows() <
instance.globalRow + this.BATCH_SIZE) {
instance.dataSheet.insertRowsAfter(
instance.dataSheet.getMaxRows(), instance.BATCH_SIZE);
}
instance.flush();
instance.globalRow += instance.cells.length;
instance.cells = instance.dataSheet.getRange(
instance.globalRow, 2, instance.BATCH_SIZE, 6).getValues();
instance.localRow = 0;
}
if (instance.globalRow >= instance.MAX_ROWS) {
Logger.log('WARNING: maximum length of the spreadsheet exceeded. ' +
'Exiting.');
throw '';
}
}
function findEmptyRow(instance) {
for (; instance.localRow < instance.cells.length &&
!(instance.cells[instance.localRow][0] == null ||
instance.cells[instance.localRow][0].length == 0); instance.localRow++);
return instance.localRow < instance.cells.length;
}
}
andrea
Da sempre appassionato di lettura e scrittura, durante l'università mi ritrovo ad unire questa passione con il mondo del web. Come tante cose strane che accadono nella vita ad un tratto la passione diventa lavoro. Così mi ritrovo Copywriter (prima) e, dopo qualche anno, anche Seo, Sem e Social Media specialist. Insomma, la fame di informarmi e capire non mi abbandona mai!