ランディング ページにアクセスしたモバイル ユーザーに優れたエクスペリエンスを提供するには、そのサイトがモバイルを念頭に置いて構築されている必要があります。サイトをモバイル向けに最適化することで、ユーザーのエンゲージメントを維持できるため、広告の効果が高まります。
PageSpeed Insights を使用したモバイル分析では、モバイル デバイスでのランディング ページの利便性を改善する方法を提案するレポートが提供されます。このソリューションでは、簡単な設定変更で、モバイルではなくデスクトップ分析を行う機能も用意されています。
仕組み
スクリプトは、モバイルの最終ページ URL(利用可能な場合)を使用して、アカウントの広告、キーワード、サイトリンクの URL を調べます。モバイルの最終ページ URL がある場合は、デフォルトで標準 URL を使用します。
このプロセスの最初の段階では、時間の許す限り多くの URL の辞書を作成します。完了すると、スクリプトは可能な限り異なる URL を識別します。
同じテンプレートのページであっても、同じアカウント内で多数の URL から非常によく似たページが返される場合があります。以下に例を示します。
http://www.example.com/product?id=123
http://www.example.com/product?id=456
この目的に沿って、Google では相違点を次のように分類しています。
- 大きな違い
主催者は異なります。次に例を示します。
http://www.example.com/path
、http://www.test.com/path
- 差別化要因
ホストは同じだがパスは異なる。次に例を示します。
http://www.example.com/shop
、http://www.example.com/blog
- 最も変化が小さい
ホストとパスは同じですが、パラメータは異なります。次に例を示します。
http://www.example.com/shop?product=1
とhttp://www.example.com/shop?product=2
その後、PageSpeed Insights API を使用して、URL のサブセット(上記の分類によってできる限り異なるものにするため)が選択され、取得されます。
結果は、メールで送信される Google スプレッドシートに表示されます。
設定
設定手順:
- Google 広告で [アカウント] タブをクリックして [スクリプト] に移動し、左側のナビゲーションで [一括処理] > [スクリプト] を選択します。
- 新しいスクリプトを作成して名前を付けます。スクリプトを承認します。
- ソースコード全体をコピーしてスクリプトに貼り付けます。
- コード内の
EMAIL_RECIPIENTS
を更新します。 - コード内の
API_KEY
を更新します。 - 必要に応じて、スクリプトのスケジュールを設定します。
API キーを取得する:
- PageSpeed Insights のスタートガイドにアクセスします。
- [API キーの取得と使用] で [キーを取得] ボタンをクリックします。
- 続くダイアログで、プロジェクトを選択または作成します。[次へ] をクリックします。
- 発行された API キーをコピーし、スクリプトの
API_KEY
変数の値に使用します。javascript const API_KEY = 'INSERT_PAGESPEED_API_KEY_HERE';
ソースコード
// 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-scripts@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-pagespeed
const API_KEY = 'INSERT_PAGESPEED_API_KEY_HERE';
const 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']
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/1SKLXUiorvgs2VuPKX7NGvcL68pv3xEqD7ZcqsEwla4M/edit';
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
};
}