I'm trying to use the PageSpeed Script but the report is not showing all my URL's. I don't understand why this happen. Can you help me?
// 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/google-ads/scripts/docs/solutions/mobile-pagespeed * for more details.
 *
 * @author Google Ads Scripts Team [
adwords...@googlegroups.com]
 *
 * @version 2.0
 *
 * @changelog
 * - version 2.1
 *  - Fixed bug with fetching ad URLs.
 * - version 2.0
 *  - Updated to use new Google Ads scripts features.
 * - version 1.3.3
 *  - Added guidance for desktop analysis.
 * - version 1.3.2
 *  - Bugfix to improve table sorting comparator.
 * - 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/google-ads/scripts/docs/solutions/mobile-pagespeedconst API_KEY = 'AIzaSyCqcpYcYgO_cO7J9ydHAwySQg8BtA94cTE';
const EMAIL_RECIPIENTS = '
natanae...@prozeducacao.com.br';
// 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']
const EXTRA_URLS_TO_CHECK =Â
// By default, the script returns analysis of how the site performs on mobile.
// Change the following from 'mobile' to 'desktop' to perform desktop analysis.
const PLATFORM_TYPE = 'mobile';
/**
 * The URL of the template spreadsheet for each report created.
 */
const SPREADSHEET_TEMPLATE =
  '
https://docs.google.com/spreadsheets/d/17tXp6RVg530dugFVW9PTundRLU9xNwciWi6o9cCaw7w/edit#gid=1858938461';
const PAGESPEED_URL =
  '
https://www.googleapis.com/pagespeedonline/v5/runPagespeed?';
/*
 * The maximum number of Campaigns to sample within the account.
 */
const CAMPAIGN_LIMIT = 50000;
/*
 * The maximum number of Ad Groups to sample within the account.
 */
const ADGROUP_LIMIT = 50000;
/**
 * These are the sampling limits for how many of each element will be examined
 * in each AdGroup.
 */
const KEYWORD_LIMIT = 20;
const SITELINK_LIMIT = 20;
const 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.
 */
const URL_FETCH_TIME_SECS = 8 * 60;
/**
 * Specifies the amount of time in seconds required to write to and format the
 * spreadsheet.
 */
const SPREADSHEET_PREP_TIME_SECS = 4 * 60;
/**
 * Represents the number of retries to use with the PageSpeed service.
 */
const MAX_RETRIES = 3;
/**
 * Represents the regex to validate the url.
 */
const urlRegex = /^(https?:\/\/[^\/]+)([^?#]*)(.*)$/;
/**
 * The main entry point for execution.
 */
function main() {
 if (!defaultsChanged()) {
  console.log('Please change the default configuration values and retry');
  return;
 }
 const accountName = AdsApp.currentAccount().getName();
 const urlStore = getUrlsFromAccount();
 const result = getPageSpeedResultsForUrls(urlStore);
 const 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) {
 const footerStyle = 'color: #aaaaaa; font-style: italic;';
 const scriptsLink = '
https://developers.google.com/google-ads/scripts/';
 const subject = `Google Ads PageSpeed URL-Sampling Script Results - ` +
   `${getDateStringInTimeZone('dd MMM yyyy')}`;
 const htmlBody = `<html><body>` +
   `<p>Hello,</p>` +
   `<p>A Google Ads Script has run successfully and the output is ` +
   `available here:` +
   `<ul><li><a href="${url}` +
   `">Google Ads PageSpeed URL-Sampling Script Results</a></li></ul></p>` +
   `<p>Regards,</p>` +
   `<span style="${footerStyle}">This email was automatically ` +
   `generated by <a href="${scriptsLink}">Google Ads Scripts</a>.` +
   `</span></body></html>`;
 const body = `Please enable HTML to view this report.`;
 const 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) {
 const spreadsheet = SpreadsheetApp.openByUrl(SPREADSHEET_TEMPLATE).copy(name);
 spreadsheet.setSpreadsheetTimeZone(AdsApp.currentAccount().getTimeZone());
 const data = pageSpeedResult.table;
 const activeSheet = spreadsheet.getActiveSheet();
 const rowStub = spreadsheet.getRangeByName('ResultRowStub');
 const top = rowStub.getRow();
 const left = rowStub.getColumn();
 const 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) {
  const 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 Google Ads 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.
  const 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) {
  const nextRow = spreadsheet.getRangeByName('FirstErrorRow').getRow();
  const 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) {
 let count = 0;
 // Associative array for column headings and contextual help URLs.
 const headings = {};
 const errors = {};
 // Record results on a per-URL basis.
 const pageSpeedResults = {};
 let urlTotalCount = 0;
 let actualUrl;
 for (const url in urlStore) {
  if (url === 'manualUrls') {
   actualUrl = urlStore[url];
  }
  if (url === 'paths') {
   let urlValue = urlStore[url];
   for (const host in urlValue) {
    const values=urlValue[host];
    for (const value in values) {
     actualUrl = Object.values(values[value]);
     }
   }
  }
  if (hasRemainingTimeForUrlFetches()) {
   const result = getPageSpeedResultForSingleUrl(actualUrl);
   if (!result.error) {
    pageSpeedResults[actualUrl] = result.pageSpeedInfo;
    let 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.
    let columnHeadings = Object.keys(columnsResult);
    for (const columnHeading of columnHeadings) {
     if (!headings[columnHeading] || (headings[columnHeading] &&
      headings[columnHeading].length <
      columnsResult[columnHeading].length)) {
      headings[columnHeading] = columnsResult[columnHeading];
     }
    }
   } else {
    errors[actualUrl] = result.error;
   }
   count++;
  }
  urlTotalCount++;
 }
 const tableHeadings = ['URL', 'Speed', 'Usability'];
 const headingKeys = Object.keys(headings);
 for (const headingKey of headingKeys) {
  tableHeadings.push(headingKey);
 }
 const table = [];
 const pageSpeedResultsUrls = Object.keys(pageSpeedResults);
 for (const pageSpeedResultsUrl of pageSpeedResultsUrls) {
  const resultUrl = pageSpeedResultsUrl;
  const row = [toPageSpeedHyperlinkFormula(resultUrl)];
  const data = pageSpeedResults[resultUrl];
  for (let 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) {
  const f1 = isNaN(parseInt(first[1])) ? 0 : parseInt(first[1]);
  const f2 = isNaN(parseInt(first[2])) ? 0 : parseInt(first[2]);
  const s1 = isNaN(parseInt(second[1])) ? 0 : parseInt(second[1]);
  const s2 = isNaN(parseInt(second[2])) ? 0 : parseInt(second[2]);
  if (f1 + f2 < s1 + s2) {
   return -1;
  } else if (f1 + f2 > s1 + s2) {
   return 1;
  }
  return 0;
 });
 // Add hyperlinks to all column headings where they are available.
 for (let tableHeading of tableHeadings) {
  // Sheets cannot have multiple links in a single cell at the moment :-/
  if (headings[tableHeading] &&
    typeof(headings[tableHeading]) === 'object') {
   tableHeading = `=HYPERLINK("${headings[tableHeading][0]}"` +
     `,"${tableHeading}")`;
  }
 }
 // Form table from errors
 const errorTable = [];
 const errorKeys = Object.keys(errors);
 for (const errorKey of errorKeys) {
  errorTable.push([errorKey, errors[errorKey]]);
 }
 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=' + PLATFORM_TYPE +'","' + 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) {
 const urlScores = {};
 if (parsedPageSpeedResponse.lighthouseResult.categories) {
  const ruleGroups = parsedPageSpeedResponse.lighthouseResult.categories;
  // At least one of the SPEED or USABILITY properties will exist, but not
  // necessarily both.
  urlScores.Speed = ruleGroups.performance ?
    ruleGroups.performance.score : '-';
  urlScores.Usability = ruleGroups.accessibility ?
    ruleGroups.accessibility.score : '-';
 }
 if (parsedPageSpeedResponse.lighthouseResult &&
  parsedPageSpeedResponse.lighthouseResult.audits) {
  const resultParts = parsedPageSpeedResponse.lighthouseResult.audits;
  for (const partName in resultParts) {
   const part = resultParts[partName];
   urlScores[part.title] = part.score;
  }
 }
 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) {
 const columnsInfo = {};
 const performance_auditRefs =
   parsedPageSpeedResponse.lighthouseResult.categories.performance.auditRefs;
 if (parsedPageSpeedResponse.lighthouseResult &&
   parsedPageSpeedResponse.lighthouseResult.audits) {
  for (const performance_auditRef of performance_auditRefs) {
   if (performance_auditRef.weight > 0 &&
     performance_auditRef.id =='largest-contentful-paint'){
    const resultParts = parsedPageSpeedResponse.lighthouseResult.audits;
    for (const partName in resultParts) {
     for (const auditref of performance_auditRef.relevantAudits) {
      if (partName === auditref || partName === 'speed-index' ||
        partName === 'Interactive'){
       if (resultParts[partName].score &&
         resultParts[partName].score !== undefined) {
        const part= resultParts[partName];
        if (!columnsInfo[part.title]) {
         columnsInfo[part.title] = part.title;
        }
       }
      }
     }
    }
   }
  }
 }
 return columnsInfo;
}
/**
 * Extracts a suitable error message to display for a failed URL. The error
 * could be passed 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) {
 let formattedMessage = null;
 if (!errorMessage) {
  formattedMessage = 'Unknown error message';
 } else {
  try {
   const parsedError = JSON.parse(errorMessage);
   // This is the nested structure expected from PageSpeed
   if (parsedError.error && parsedError.error.errors) {
    const 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) {
 let parsedResponse = null;
 let errorMessage = null;
 let retries = 0;
 while ((!parsedResponse || parsedResponse.responseCode !== 200) &&
     retries < MAX_RETRIES) {
  errorMessage = null;
  const 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));
 }
 let columnsInfo;
 let urlScores;
 if (!errorMessage) {
   columnsInfo = extractColumnsInfo(parsedResponse);
   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 A Google Ads entity such as an Ad, Keyword or
 *   Sitelink.
 * @return {string} The URL.
 */
function getMobileUrl(entity) {
 const urls = entity.urls();
 let url = null;
 if (urls) {
  if (urls.getMobileFinalUrl()) {
   url = urls.getMobileFinalUrl();
  } else if (urls.getFinalUrl()) {
   url = urls.getFinalUrl();
  }
 }
 if (!url) {
  switch (entity.getEntityType()) {
   case 'Ad':
    url = entity.urls().getFinalUrl();
    break;
   case 'Keyword':
    url = entity.urls().getFinalUrl();
    break;
   case 'Sitelink':
   case 'AdGroupSitelink':
   case 'CampaignSitelink':
    url = entity.getLinkUrl();
    break;
   default:
    console.warn('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() {
 const remainingTime = AdsApp.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() {
 const remainingTime = AdsApp.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() {
 const urlStore = new UrlStore(EXTRA_URLS_TO_CHECK);
 const campaigns = AdsApp.campaigns()
           .forDateRange('LAST_30_DAYS')
           .withCondition('campaign.status = "ENABLED"')
           .orderBy('metrics.clicks DESC')
           .withLimit(CAMPAIGN_LIMIT)
           .get();
 while (campaigns.hasNext() && hasRemainingTimeForAccountIteration()) {
  const campaign = campaigns.next();
  let campaignUrls = getUrlsFromCampaign(campaign);
  urlStore.addUrls(campaignUrls);
 }
 const adGroups = AdsApp.adGroups()
           .forDateRange('LAST_30_DAYS')
           .withCondition('ad_group.status = "ENABLED"')
           .orderBy('metrics.clicks DESC')
           .withLimit(ADGROUP_LIMIT)
           .get();
 while (adGroups.hasNext() && hasRemainingTimeForAccountIteration()) {
  const adGroup = adGroups.next();
  const 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) {
 const uniqueUrls = {};
 const sitelinks =
   adGroup.extensions().sitelinks().withLimit(SITELINK_LIMIT).get();
 for (const sitelink of sitelinks) {
  const 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) {
 const uniqueUrls = {};
 let url = null;
 const sitelinks = campaign
   .extensions().sitelinks().withLimit(SITELINK_LIMIT).get();
 for (const sitelink of sitelinks) {
  url = getMobileUrl(sitelink);
  if (url) {
   uniqueUrls[url] = true;
  }
 }
 const ads = AdsApp.ads().forDateRange('LAST_30_DAYS')
   .withCondition('ad_group_ad.status = "ENABLED"')
   .orderBy('metrics.clicks DESC')
   .withLimit(AD_LIMIT)
   .get();
 for (const ad of ads) {
  url = getMobileUrl(ad);
  if (url) {
   uniqueUrls[url] = true;
  }
 }
 const keywords =
   AdsApp.keywords().forDateRange('LAST_30_DAYS')
   .withCondition('campaign.status = "ENABLED"')
   .orderBy('metrics.clicks DESC')
   .withLimit(KEYWORD_LIMIT)
   .get();
 for (const keyword of keywords) {
  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 || AdsApp.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 manualUrls are supplied at construction time, these will take
 * precedence over URLs added subsequently to the object.
 */
class UrlStore {
 /**
  * @param {?Array.<string>=} manualUrls An optional list of URLs to check.
  */
 constructor (manualUrls) {
  this.manualUrls = manualUrls;
  this.paths = {};
 }
 /**
  * Adds a URL to the UrlStore.
  *
  * @param {string} url The URL to add.
  */
 addUrl(url) {
  if (!url || this.manualUrls.indexOf(url) > -1) {
   return;
  }
  const matches = urlRegex.exec(url);
  if (matches) {
   let host = matches[1];
   let path = matches[2];
   if (!this.paths[host]) {
    this.paths[host] = {};
   }
   let hostObj = this.paths[host];
   if (!path) {
    path = '/';
   }
   if (!hostObj[path]) {
    hostObj[path] = {};
   }
   let pathObj = hostObj[path];
   pathObj[url] = url;
  }
 }
 /**
  * Adds multiple URLs to the UrlStore.
  *
  * @param {!Array.<string>} urls The URLs to add.
  */
 addUrls(urls) {
  for (const url of urls) {
   this.addUrl(url);
  }
 }
 /**
  * Creates and returns an iterator that tries to iterate over all available
  * URLs.
  *
  * @return {!UrlStoreIterator} The new iterator object.
  */
 __iterator__ () {
  return new UrlStoreIterator(this.paths, this.manualUrls);
 }
}
/**
 * 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.
 */
const 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 {
   let e = a.pop();
   a.unshift(e);
  }
 }
 function pick_(a) {
  if (typeof a[0] === 'string') {
   return a.shift();
  } else {
   let element = pick_(a[0]);
   if (!a[0].length) {
    a.shift();
   } else {
    rotate_(a);
   }
   return element;
  }
 }
 function objectToArray_(obj) {
  if (typeof obj !== 'object') {
   return obj;
  }
  const a = [];
  for (let 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) {
 let result = null;
 let error = null;
 const fullUrl = PAGESPEED_URL + 'key=' + API_KEY + '&url=' + encodeURI(url) +
        '&strategy=mobile&category=ACCESSIBILITY&category=PERFORMANCE';
 const params = {muteHttpExceptions: true};
 try {
  const 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
 };
}