PageSpeed Insights: Mobile Analysis - Single Account

This script is for a single account. For operating on multiple accounts in a Manager Account, use the Manager Account version of the script.

Ensuring a positive experience for mobile users when they visit landing pages requires that sites are built with mobile in mind. A site well-optimized for mobile will ensure that users remain engaged with your site, making your ads more effective.

Mobile Analysis using PageSpeed Insights provides a report suggesting ways to improve the landing page experience on mobile.

How it works

The script examines URLs from the account - from Ads, Keywords and Sitelinks - using mobile final URLs if available, but defaulting to standard URLs otherwise.

The first stage of the process is to build up a dictionary of as many URLs as time will allow.

Once this has completed, the script looks to identify URLs that are as different as possible - it is possible to imagine the scenario where a large number of URLs in an account return very similar pages. For example:

http://www.example.com/product?id=123 and http://www.example.com/product?id=456

very probably return pages created from the same template. To this end, the difference is classified as follows:

  • Most different - hosts are different - e.g. http://www.example.com/path and http://www.test.com/path.
  • More different - hosts are same, but paths differ - e.g. http://www.example.com/shop and http://www.example.com/blog.
  • Least different - hosts and paths the same, parameters differ - e.g. http://www.example.com/shop?product=1 and http://www.example.com/shop?product=2.

A smaller subset of URLs - with the aim of being as different as possible by the above classification - are then selected and retrieved via the PageSpeed Insights API.

The results are presented in a Google Spreadsheet, which is sent by email.

Setup

Source code

// Copyright 2015, Google Inc. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Mobile Performance from PageSpeed Insights - Single Account
 *
 * Produces a report showing how well landing pages are set up for mobile
 * devices and highlights potential areas for improvement. See :
 * https://developers.google.com/adwords/scripts/docs/solutions/mobile-pagespeed
 * for more details.
 *
 * @author AdWords Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 1.3.1
 *
 * @changelog
 * - version 1.3.1
 *   - Bugfix for handling the absence of optional values in PageSpeed response.
 * - version 1.3
 *   - Removed the need for the user to take a copy of the spreadsheet.
 *   - Added the ability to customize the Campaign and Ad Group limits.
 * - version 1.2.1
 *   - Improvements to time zone handling.
 * - version 1.2
 *   - Bug fix for handling empty results from URLs.
 *   - Error reporting in spreadsheet for failed URL fetches.
 * - version 1.1
 *   - Updated the comments to be in sync with the guide.
 * - version 1.0
 *   - Released initial version.
 */

// See "Obtaining an API key" at
// https://developers.google.com/adwords/scripts/docs/solutions/mobile-pagespeed
var API_KEY = 'INSERT_PAGESPEED_API_KEY_HERE';
var EMAIL_RECIPIENTS = ['INSERT_EMAIL_ADDRESS_HERE'];

// If you wish to add extra URLs to checked, list them here as a
// comma-separated list eg. ['http://abc.xyz', 'http://www.google.com']
var EXTRA_URLS_TO_CHECK = [];

/**
 * The URL of the template spreadsheet for each report created.
 */
var SPREADSHEET_TEMPLATE =
    'https://docs.google.com/spreadsheets/d/1SKLXUiorvgs2VuPKX7NGvcL68pv3xEqD7ZcqsEwla4M/edit';

var PAGESPEED_URL =
    'https://www.googleapis.com/pagespeedonline/v2/runPagespeed?';

/*
 * The maximum number of Campaigns to sample within the account.
 */
var CAMPAIGN_LIMIT = 50000;

/*
 * The maximum number of Ad Groups to sample within the account.
 */
var ADGROUP_LIMIT = 50000;

/**
 * These are the sampling limits for how many of each element will be examined
 * in each AdGroup.
 */
var KEYWORD_LIMIT = 20;
var SITELINK_LIMIT = 20;
var AD_LIMIT = 30;

/**
 * Specifies the amount of time in seconds required to do the URL fetching and
 * result generation. As this is the last step, entities in the account will be
 * iterated over until this point.
 */
var URL_FETCH_TIME_SECS = 8 * 60;

/**
 * Specifies the amount of time in seconds required to write to and format the
 * spreadsheet.
 */
var SPREADSHEET_PREP_TIME_SECS = 4 * 60;

/**
 * Represents the number of retries to use with the PageSpeed service.
 */
var MAX_RETRIES = 3;

/**
 * The main entry point for execution.
 */
function main() {
  if (!defaultsChanged()) {
    Logger.log('Please change the default configuration values and retry');
    return;
  }
  var accountName = AdWordsApp.currentAccount().getName();
  var urlStore = getUrlsFromAccount();
  var result = getPageSpeedResultsForUrls(urlStore);
  var spreadsheet = createPageSpeedSpreadsheet(accountName +
      ': PageSpeed Insights - Mobile Analysis', result);
  spreadsheet.addEditors(EMAIL_RECIPIENTS);
  sendEmail(spreadsheet.getUrl());
}

/**
 * Sends an email to the user with the results of the run.
 *
 * @param {string} url URL of the spreadsheet.
 */
function sendEmail(url) {
  var footerStyle = 'color: #aaaaaa; font-style: italic;';
  var scriptsLink = 'https://developers.google.com/adwords/scripts/';
  var subject = 'AdWords PageSpeed URL-Sampling Script Results - ' +
      getDateStringInTimeZone('dd MMM yyyy');
  var htmlBody = '<html><body>' +
      '<p>Hello,</p>' +
      '<p>An AdWords Script has run successfully and the output is available ' +
      'here:' +
      '<ul><li><a href="' + url +
      '">AdWords PageSpeed URL-Sampling Script Results</a></li></ul></p>' +
      '<p>Regards,</p>' +
      '<span style="' + footerStyle + '">This email was automatically ' +
      'generated by <a href="' + scriptsLink + '">AdWords Scripts</a>.<span>' +
      '</body></html>';
  var body = 'Please enable HTML to view this report.';
  var options = {htmlBody: htmlBody};
  MailApp.sendEmail(EMAIL_RECIPIENTS, subject, body, options);
}

/**
 * Checks to see that placeholder defaults have been changed.
 *
 * @return {boolean} true if placeholders have been changed, false otherwise.
 */
function defaultsChanged() {
  if (API_KEY == 'INSERT_PAGESPEED_API_KEY_HERE' ||
      SPREADSHEET_TEMPLATE == 'INSERT_SPREADSHEET_URL_HERE' ||
      JSON.stringify(EMAIL_RECIPIENTS) ==
      JSON.stringify(['INSERT_EMAIL_ADDRESS_HERE'])) {
    return false;
  }
  return true;
}

/**
 * Creates a new PageSpeed spreadsheet and populates it with result data.
 *
 * @param {string} name The name to give to the spreadsheet.
 * @param {Object} pageSpeedResult The result from PageSpeed, and the number of
 *     URLs that could have been chosen from.
 * @return {Spreadsheet} The newly-created spreadsheet.
 */
function createPageSpeedSpreadsheet(name, pageSpeedResult) {
  var spreadsheet = SpreadsheetApp.openByUrl(SPREADSHEET_TEMPLATE).copy(name);
  spreadsheet.setSpreadsheetTimeZone(AdWordsApp.currentAccount().getTimeZone());
  var data = pageSpeedResult.table;
  var activeSheet = spreadsheet.getActiveSheet();
  var rowStub = spreadsheet.getRangeByName('ResultRowStub');
  var top = rowStub.getRow();
  var left = rowStub.getColumn();
  var cols = rowStub.getNumColumns();
  if (data.length > 2) { // No need to extend the template if num rows <= 2
    activeSheet.insertRowsAfter(
          spreadsheet.getRangeByName('EmptyUrlRow').getRow(), data.length);
    rowStub.copyTo(activeSheet.getRange(top + 1, left, data.length - 2, cols));
  }
  // Extend the formulas and headings to accommodate the data.
  if (data.length && data[0].length > 4) {
    var metricsRange = activeSheet
        .getRange(top - 6, left + cols, data.length + 5, data[0].length - 4);
    activeSheet.getRange(top - 6, left + cols - 1, data.length + 5)
        .copyTo(metricsRange);
    // Paste in the data values.
    activeSheet.getRange(top - 1, left, data.length, data[0].length)
        .setValues(data);
    // Move the 'Powered by AdWords Scripts' to right corner of table.
    spreadsheet.getRangeByName('PoweredByText').moveTo(activeSheet.getRange(1,
      data[0].length + 1, 1, 1));
    // Set summary - date and number of URLs chosen from.
    var summaryDate = getDateStringInTimeZone('dd MMM yyyy');
    spreadsheet.getRangeByName('SummaryDate').setValue('Summary as of ' +
        summaryDate + '. Results drawn from ' + pageSpeedResult.totalUrls +
        ' URLs.');
  }
  // Add errors if they exist
  if (pageSpeedResult.errors.length) {
    var nextRow = spreadsheet.getRangeByName('FirstErrorRow').getRow();
    var errorRange = activeSheet.getRange(nextRow, 2,
        pageSpeedResult.errors.length, 2);
    errorRange.setValues(pageSpeedResult.errors);
  }
  return spreadsheet;
}

/**
 * This function takes a collection of URLs as provided by the UrlStore object
 * and gets results from the PageSpeed service. However, two important things :
 *     (1) It only processes a handful, as determined by URL_LIMIT.
 *     (2) The URLs returned from iterating on the UrlStore are in a specific
 *     order, designed to produce as much variety as possible (removing the need
 *     to process all the URLs in an account.
 *
 * @param {UrlStore} urlStore Object containing URLs to process.
 * @return {Object} An object with three properties: 'table' - the 2d table of
 *     results, 'totalUrls' - the number of URLs chosen from, and errors.
 */
function getPageSpeedResultsForUrls(urlStore) {
  var count = 0;
  // Associative array for column headings and contextual help URLs.
  var headings = {};
  var errors = {};
  // Record results on a per-URL basis.
  var pageSpeedResults = {};
  var urlTotalCount = 0;

  for (var url in urlStore) {
    if (hasRemainingTimeForUrlFetches()) {
      var result = getPageSpeedResultForSingleUrl(url);
      if (!result.error) {
        pageSpeedResults[url] = result.pageSpeedInfo;
        var columnsResult = result.columnsInfo;
        // Loop through each heading element; the PageSpeed Insights API
        // doesn't always return URLs for each column heading, so aggregate
        // these across each call to get the most complete list.
        var columnHeadings = Object.keys(columnsResult);
        for (var i = 0, lenI = columnHeadings.length; i < lenI; i++) {
          var columnHeading = columnHeadings[i];
          if (!headings[columnHeading] || (headings[columnHeading] &&
            headings[columnHeading].length <
            columnsResult[columnHeading].length)) {
            headings[columnHeading] = columnsResult[columnHeading];
          }
        }
      } else {
        errors[url] = result.error;
      }
      count++;
    }
    urlTotalCount++;
  }

  var tableHeadings = ['URL', 'Speed', 'Usability'];
  var headingKeys = Object.keys(headings);
  for (var y = 0, lenY = headingKeys.length; y < lenY; y++) {
    tableHeadings.push(headingKeys[y]);
  }

  var table = [];
  var pageSpeedResultsUrls = Object.keys(pageSpeedResults);
  for (var r = 0, lenR = pageSpeedResultsUrls.length; r < lenR; r++) {
    var resultUrl = pageSpeedResultsUrls[r];
    var row = [toPageSpeedHyperlinkFormula(resultUrl)];
    var data = pageSpeedResults[resultUrl];
    for (var j = 1, lenJ = tableHeadings.length; j < lenJ; j++) {
      row.push(data[tableHeadings[j]]);
    }
    table.push(row);
  }
  // Present the table back in the order worst-performing-first.
  table.sort(function(first, second) {
    if (first[1] + first[2] < second[1] + second[2]) {
      return -1;
    } else if (first[1] + first[2] > second[1] + second[2]) {
      return 1;
    }
    return 0;
  });

  // Add hyperlinks to all column headings where they are available.
  for (var h = 0, lenH = tableHeadings.length; h < lenH; h++) {
    // Sheets cannot have multiple links in a single cell at the moment :-/
    if (headings[tableHeadings[h]] &&
        typeof(headings[tableHeadings[h]]) === 'object') {
      tableHeadings[h] = '=HYPERLINK("' + headings[tableHeadings[h]][0] +
          '","' + tableHeadings[h] + '")';
    }
  }

  // Form table from errors
  var errorTable = [];
  var errorKeys = Object.keys(errors);
  for (var k = 0; k < errorKeys.length; k++) {
    errorTable.push([errorKeys[k], errors[errorKeys[k]]]);
  }
  table.unshift(tableHeadings);
  return {
    table: table,
    totalUrls: urlTotalCount,
    errors: errorTable
  };
}

/**
 * Given a URL, returns a spreadsheet formula that displays the URL yet links to
 * the PageSpeed URL for examining this.
 *
 * @param {string} url The URL to embed in the Hyperlink formula.
 * @return {string} A string representation of the spreadsheet formula.
 */
function toPageSpeedHyperlinkFormula(url) {
  return '=HYPERLINK("' +
         'https://developers.google.com/speed/pagespeed/insights/?url=' + url +
         '&tab=mobile","' + url + '")';
}

/**
 * Creates an object of results metrics from the parsed results of a call to
 * the PageSpeed service.
 *
 * @param {Object} parsedPageSpeedResponse The object returned from PageSpeed.
 * @return {Object} An associative array with entries for each metric.
 */
function extractResultRow(parsedPageSpeedResponse) {
  var urlScores = {};
  if (parsedPageSpeedResponse.ruleGroups) {
    var ruleGroups = parsedPageSpeedResponse.ruleGroups;
    // At least one of the SPEED or USABILITY properties will exist, but not
    // necessarily both.
    urlScores.Speed = ruleGroups.SPEED ? ruleGroups.SPEED.score : '-';
    urlScores.Usability = ruleGroups.USABILITY ?
        ruleGroups.USABILITY.score : '-';
  }
  if (parsedPageSpeedResponse.formattedResults &&
    parsedPageSpeedResponse.formattedResults.ruleResults) {
    var resultParts = parsedPageSpeedResponse.formattedResults.ruleResults;
    for (var partName in resultParts) {
      var part = resultParts[partName];
      urlScores[part.localizedRuleName] = part.ruleImpact;
    }
  }
  return urlScores;
}

/**
 * Extracts the headings for the metrics returned from PageSpeed, and any
 * associated help URLs.
 *
 * @param {Object} parsedPageSpeedResponse The object returned from PageSpeed.
 * @return {Object} An associative array used to store column-headings seen in
 *     the response. This can take two forms:
 *     (1) {'heading':'heading', ...} - this form is where no help URLs are
 *     known.
 *     (2) {'heading': [url1, ...]} - where one or more URLs is returned that
 *     provides help on the particular heading item.
 */
function extractColumnsInfo(parsedPageSpeedResponse) {
  var columnsInfo = {};
  if (parsedPageSpeedResponse.formattedResults &&
    parsedPageSpeedResponse.formattedResults.ruleResults) {
    var resultParts = parsedPageSpeedResponse.formattedResults.ruleResults;
    for (var partName in resultParts) {
      var part = resultParts[partName];
      if (!columnsInfo[part.localizedRuleName]) {
        columnsInfo[part.localizedRuleName] = part.localizedRuleName;
      }
      // Find help URLs in the response
      var summary = part.summary;
      if (summary && summary.args) {
        var argList = summary.args;
        for (var i = 0, lenI = argList.length; i < lenI; i++) {
          var arg = argList[i];
          if ((arg.type) && (arg.type == 'HYPERLINK') &&
              (arg.key) && (arg.key == 'LINK') &&
              (arg.value)) {
            columnsInfo[part.localizedRuleName] = [arg.value];
          }
        }
      }
      if (part.urlBlocks) {
        var blocks = part.urlBlocks;
        var urls = [];
        for (var j = 0, lenJ = blocks.length; j < lenJ; j++) {
          var block = blocks[j];
          if (block.header) {
            var header = block.header;
            if (header.args) {
              var args = header.args;
              for (var k = 0, lenK = args.length; k < lenK; k++) {
                var argument = args[k];
                if ((argument.type) &&
                    (argument.type == 'HYPERLINK') &&
                    (argument.key) &&
                    (argument.key == 'LINK') &&
                    (argument.value)) {
                  urls.push(argument.value);
                }
              }
            }
          }
        }
        if (urls.length > 0) {
          columnsInfo[part.localizedRuleName] = urls;
        }
      }
    }
  }
  return columnsInfo;
}

/**
 * Extracts a suitable error message to display for a failed URL. The error
 * could be passed in in the nested PageSpeed error format, or there could have
 * been a more fundamental error in the fetching of the URL. Extract the
 * relevant message in each case.
 *
 * @param {string} errorMessage The error string.
 * @return {string} A formatted error message.
 */
function formatErrorMessage(errorMessage) {
  var formattedMessage = null;
  if (!errorMessage) {
    formattedMessage = 'Unknown error message';
  } else {
    try {
      var parsedError = JSON.parse(errorMessage);
      // This is the nested structure expected from PageSpeed
      if (parsedError.error && parsedError.error.errors) {
        var firstError = parsedError.error.errors[0];
        formattedMessage = firstError.message;
      } else if (parsedError.message) {
        formattedMessage = parsedError.message;
      } else {
        formattedMessage = errorMessage.toString();
      }
    } catch (e) {
      formattedMessage = errorMessage.toString();
    }
  }
  return formattedMessage;
}

/**
 * Calls the PageSpeed API for a single URL, and attempts to parse the resulting
 * JSON. If successful, produces an object for the metrics returned, and an
 * object detailing the headings and help URLs seen.
 *
 * @param {string} url The URL to run PageSpeed for.
 * @return {Object} An object with pageSpeed metrics, column-heading info
 *     and error properties.
 */
function getPageSpeedResultForSingleUrl(url) {
  var parsedResponse = null;
  var errorMessage = null;
  var retries = 0;

  while ((!parsedResponse || parsedResponse.responseCode !== 200) &&
         retries < MAX_RETRIES) {
    errorMessage = null;
    var fetchResult = checkUrl(url);
    if (fetchResult.responseText) {
      try {
        parsedResponse = JSON.parse(fetchResult.responseText);
        break;
      } catch (e) {
        errorMessage = formatErrorMessage(e);
      }
    } else {
      errorMessage = formatErrorMessage(fetchResult.error);
    }
    retries++;
    Utilities.sleep(1000 * Math.pow(2, retries));
  }
  if (!errorMessage) {
    var columnsInfo = extractColumnsInfo(parsedResponse);
    var urlScores = extractResultRow(parsedResponse);
  }
  return {
    pageSpeedInfo: urlScores,
    columnsInfo: columnsInfo,
    error: errorMessage
  };
}

/**
 * Gets the most representative URL that would be used on a mobile device
 * taking into account Upgraded URLs.
 *
 * @param {Entity} entity An AdWords entity such as an Ad, Keyword or Sitelink.
 * @return {string} The URL.
 */
function getMobileUrl(entity) {
  var urls = entity.urls();
  var url = null;
  if (urls) {
    if (urls.getMobileFinalUrl()) {
      url = urls.getMobileFinalUrl();
    } else if (urls.getFinalUrl()) {
      url = urls.getFinalUrl();
    }
  }
  if (!url) {
    switch (entity.getEntityType()) {
      case 'Ad':
      case 'Keyword':
        url = entity.getDestinationUrl();
        break;
      case 'Sitelink':
      case 'AdGroupSitelink':
      case 'CampaignSitelink':
        url = entity.getLinkUrl();
        break;
      default:
        Logger.log('No URL found' + entity.getEntityType());
    }
  }
  if (url) {
    url = encodeURI(decodeURIComponent(url));
  }
  return url;
}

/**
 * Determines whether there is enough remaining time to continue iterating
 * through the account.
 *
 * @return {boolean} Returns true if there is enough time remaining to continue
 *     iterating.
 */
function hasRemainingTimeForAccountIteration() {
  var remainingTime = AdWordsApp.getExecutionInfo().getRemainingTime();
  return remainingTime > SPREADSHEET_PREP_TIME_SECS + URL_FETCH_TIME_SECS;
}

/**
 * Determines whether there is enough remaining time to continue fetching URLs.
 *
 * @return {boolean} Returns true if there is enough time remaining to continue
 *     fetching.
 */
function hasRemainingTimeForUrlFetches() {
  var remainingTime = AdWordsApp.getExecutionInfo().getRemainingTime();
  return remainingTime > SPREADSHEET_PREP_TIME_SECS;
}

/**
 * Iterates through all the available Campaigns and AdGroups, to a limit of
 * defined in CAMPAIGN_LIMIT and ADGROUP_LIMIT until the time limit is reached
 * allowing enough time for the post-iteration steps, e.g. fetching and
 * analysing URLs and building results.
 *
 * @return {UrlStore} An UrlStore object with URLs from the account.
 */
function getUrlsFromAccount() {
  var urlStore = new UrlStore(EXTRA_URLS_TO_CHECK);
  var campaigns = AdWordsApp.campaigns()
                      .forDateRange('LAST_30_DAYS')
                      .withCondition('Status = "ENABLED"')
                      .orderBy('Clicks DESC')
                      .withLimit(CAMPAIGN_LIMIT)
                      .get();
  while (campaigns.hasNext() && hasRemainingTimeForAccountIteration()) {
    var campaign = campaigns.next();
    var campaignUrls = getUrlsFromCampaign(campaign);
    urlStore.addUrls(campaignUrls);
  }
  var adGroups = AdWordsApp.adGroups()
                     .forDateRange('LAST_30_DAYS')
                     .withCondition('Status = "ENABLED"')
                     .orderBy('Clicks DESC')
                     .withLimit(ADGROUP_LIMIT)
                     .get();
  while (adGroups.hasNext() && hasRemainingTimeForAccountIteration()) {
    var adGroup = adGroups.next();
    var adGroupUrls = getUrlsFromAdGroup(adGroup);
    urlStore.addUrls(adGroupUrls);
  }
  return urlStore;
}

/**
 * Work through an ad group's members in the account, but only up to the maximum
 * specified by the SITELINK_LIMIT.
 *
 * @param {AdGroup} adGroup The adGroup to process.
 * @return {!Array.<string>} A list of URLs.
 */
function getUrlsFromAdGroup(adGroup) {
  var uniqueUrls = {};
  var sitelinks =
      adGroup.extensions().sitelinks().withLimit(SITELINK_LIMIT).get();
  while (sitelinks.hasNext()) {
    var sitelink = sitelinks.next();
    var url = getMobileUrl(sitelink);
    if (url) {
      uniqueUrls[url] = true;
    }
  }
  return Object.keys(uniqueUrls);
}

/**
 * Work through a campaign's members in the account, but only up to the maximum
 * specified by the AD_LIMIT, KEYWORD_LIMIT and SITELINK_LIMIT.
 *
 * @param {Campaign} campaign The campaign to process.
 * @return {!Array.<string>} A list of URLs.
 */
function getUrlsFromCampaign(campaign) {
  var uniqueUrls = {};
  var url = null;
  var sitelinks = campaign
      .extensions().sitelinks().withLimit(SITELINK_LIMIT).get();
  while (sitelinks.hasNext()) {
    var sitelink = sitelinks.next();
    url = getMobileUrl(sitelink);
    if (url) {
      uniqueUrls[url] = true;
    }
  }
  var ads = campaign.ads().forDateRange('LAST_30_DAYS')
      .withCondition('Status = "ENABLED"')
      .orderBy('Clicks DESC')
      .withLimit(AD_LIMIT)
      .get();
  while (ads.hasNext()) {
    var ad = ads.next();
    url = getMobileUrl(ad);
    if (url) {
      uniqueUrls[url] = true;
    }
  }
  var keywords = campaign.keywords().forDateRange('LAST_30_DAYS')
      .withCondition('Status = "ENABLED"')
      .orderBy('Clicks DESC')
      .withLimit(KEYWORD_LIMIT)
      .get();
  while (keywords.hasNext()) {
    var keyword = keywords.next();
    url = getMobileUrl(keyword);
    if (url) {
      uniqueUrls[url] = true;
    }
  }
  return Object.keys(uniqueUrls);
}

/**
 * Produces a formatted string representing a given date in a given time zone.
 *
 * @param {string} format A format specifier for the string to be produced.
 * @param {Date} date A date object. Defaults to the current date.
 * @param {string} timeZone A time zone. Defaults to the account's time zone.
 * @return {string} A formatted string of the given date in the given time zone.
 */
function getDateStringInTimeZone(format, date, timeZone) {
  date = date || new Date();
  timeZone = timeZone || AdWordsApp.currentAccount().getTimeZone();
  return Utilities.formatDate(date, timeZone, format);
}

/**
 * UrlStore - this is an object that takes URLs, added one by one, and then
 * allows them to be iterated through in a particular order, which aims to
 * maximise the variety between the returned URLs.
 *
 * This works by splitting the URL into three parts: host, path and params
 * In comparing two URLs, most weight is given if the hosts differ, then if the
 * paths differ, and finally if the params differ.
 *
 * UrlStore sets up a tree with 3 levels corresponding to the above. The full
 * URL exists at the leaf level. When a request is made for an iterator, a copy
 * is taken, and a path through the tree is taken, using the first host. Each
 * entry is removed from the tree as it is used, and the layers are rotated with
 * each call such that the next call will result in a different host being used
 * (where possible).
 *
 * Where opt_manualUrls are supplied at construction time, these will take
 * precedence over URLs added subsequently to the object.
 *
 * @param {?Array.<string>=} opt_manualUrls An optional list of URLs to check.
 * @constructor
 */
function UrlStore(opt_manualUrls) {
  this.manualUrls = opt_manualUrls || [];
  this.paths = {};
  this.re = /^(https?:\/\/[^\/]+)([^?#]*)(.*)$/;
}

/**
 * Adds a URL to the UrlStore.
 *
 * @param {string} url The URL to add.
 */
UrlStore.prototype.addUrl = function(url) {
  if (!url || this.manualUrls.indexOf(url) > -1) {
    return;
  }
  var matches = this.re.exec(url);
  if (matches) {
    var host = matches[1];
    var path = matches[2];
    var param = matches[3];
    if (!this.paths[host]) {
      this.paths[host] = {};
    }
    var hostObj = this.paths[host];
    if (!path) {
      path = '/';
    }
    if (!hostObj[path]) {
      hostObj[path] = {};
    }
    var pathObj = hostObj[path];
    pathObj[url] = url;
  }
};

/**
 * Adds multiple URLs to the UrlStore.
 *
 * @param {!Array.<string>} urls The URLs to add.
 */
UrlStore.prototype.addUrls = function(urls) {
  for (var i = 0; i < urls.length; i++) {
    this.addUrl(urls[i]);
  }
};

/**
 * Creates and returns an iterator that tries to iterate over all available
 * URLs return them in an order to maximise the difference between them.
 *
 * @return {UrlStoreIterator} The new iterator object.
 */
UrlStore.prototype.__iterator__ = function() {
  return new UrlStoreIterator(this.paths, this.manualUrls);
};

var UrlStoreIterator = (function() {
  function UrlStoreIterator(paths, manualUrls) {
    this.manualUrls = manualUrls.slice();
    this.urls = objectToArray_(paths);
  }
  UrlStoreIterator.prototype.next = function() {
    if (this.manualUrls.length) {
      return this.manualUrls.shift();
    }
    if (this.urls.length) {
      return pick_(this.urls);
    } else {
      throw StopIteration;
    }
  };
  function rotate_(a) {
    if (a.length < 2) {
      return a;
    } else {
      var e = a.pop();
      a.unshift(e);
    }
  }
  function pick_(a) {
    if (typeof a[0] === 'string') {
      return a.shift();
    } else {
      var element = pick_(a[0]);
      if (!a[0].length) {
        a.shift();
      } else {
        rotate_(a);
      }
      return element;
    }
  }

  function objectToArray_(obj) {
    if (typeof obj !== 'object') {
      return obj;
    }

    var a = [];
    for (var k in obj) {
      a.push(objectToArray_(obj[k]));
    }
    return a;
  }
  return UrlStoreIterator;
})();

/**
 * Runs the PageSpeed fetch.
 *
 * @param {string} url
 * @return {Object} An object containing either the successful response from the
 *     server, or an error message.
 */
function checkUrl(url) {
  var result = null;
  var error = null;
  var fullUrl = PAGESPEED_URL + 'key=' + API_KEY + '&url=' + encodeURI(url) +
                '&prettyprint=false&strategy=mobile';
  var params = {muteHttpExceptions: true};
  try {
    var pageSpeedResponse = UrlFetchApp.fetch(fullUrl, params);
    if (pageSpeedResponse.getResponseCode() === 200) {
      result = pageSpeedResponse.getContentText();
    } else {
      error = pageSpeedResponse.getContentText();
    }
  } catch (e) {
    error = e.message;
  }
  return {
    responseText: result,
    error: error
  };
}

Looking for the Manager Account (MCC) version? Click here

Send feedback about...

AdWords Scripts
AdWords Scripts
Need help? Visit our support page.