Google Ads Script (JavaScript)
/**
* Goodlabs Google Ads script: broken landing page checker
* Author: Goodlabs (https://goodlabs.kz)
* Schedule: Daily at 06:00
*/
var CONFIG = {
SPREADSHEET_URL: "https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/edit",
NOTIFICATION_EMAIL: "analytics@yourdomain.com",
PAUSE_BROKEN_ADS: false,
OUT_OF_STOCK_REGEX: "Out of stock|Товар распродан|Нет в наличии",
TIMEOUT_MS: 10000
};
function main() {
var brokenCount = 0;
var sheet = getOrCreateSheet(CONFIG.SPREADSHEET_URL);
var checkedUrls = {};
Logger.log("Starting Goodlabs Broken URL Audit...");
var adIterator = AdsApp.ads()
.withCondition("Status = ENABLED")
.withCondition("CampaignStatus = ENABLED")
.withCondition("AdGroupStatus = ENABLED")
.get();
while (adIterator.hasNext()) {
var ad = adIterator.next();
var url = ad.urls().getFinalUrl();
if (!url || checkedUrls[url]) continue;
var result = checkUrl(url);
checkedUrls[url] = result;
if (!result.ok) {
brokenCount++;
sheet.appendRow([
new Date(),
ad.getCampaign().getName(),
ad.getAdGroup().getName(),
ad.getId(),
url,
result.statusCode,
result.error
]);
if (CONFIG.PAUSE_BROKEN_ADS) {
ad.pause();
Logger.log("PAUSED broken ad: " + ad.getId() + " (" + url + ")");
}
}
}
Logger.log("Audit finished. Broken URLs detected: " + brokenCount);
if (brokenCount > 0 && CONFIG.NOTIFICATION_EMAIL) {
MailApp.sendEmail(
CONFIG.NOTIFICATION_EMAIL,
"⚠️ Google Ads Alert: " + brokenCount + " Broken URLs Found",
"The broken landing page check found " + brokenCount + " failing landing pages in your Google Ads account.\n\n" +
"Review the full report here: " + CONFIG.SPREADSHEET_URL
);
}
}
function checkUrl(url) {
try {
var response = UrlFetchApp.fetch(url, {
muteHttpExceptions: true,
followRedirects: true,
validateHttpsCertificates: true,
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" }
});
var code = response.getResponseCode();
if (code >= 400) {
return { ok: false, statusCode: code, error: "HTTP Error " + code };
}
var text = response.getContentText();
var oosPattern = new RegExp(CONFIG.OUT_OF_STOCK_REGEX, "i");
if (CONFIG.OUT_OF_STOCK_REGEX && oosPattern.test(text)) {
return { ok: false, statusCode: code, error: "Out of Stock Detected" };
}
return { ok: true, statusCode: code, error: "" };
} catch (err) {
return { ok: false, statusCode: 0, error: err.toString() };
}
}
function getOrCreateSheet(url) {
try {
var ss = SpreadsheetApp.openByUrl(url);
var sheet = ss.getActiveSheet();
if (sheet.getLastRow() === 0) {
sheet.appendRow(["Timestamp", "Campaign", "Ad Group", "Ad ID", "Final URL", "HTTP Status", "Error Details"]);
sheet.getRange(1, 1, 1, 7).setFontWeight("bold").setBackground("#f3f4f6");
}
return sheet;
} catch(e) {
Logger.log("Spreadsheet error: " + e);
return { appendRow: function() {} };
}
}How it works
- Iterates over all active Ads, Keywords, and Sitelink extensions using
AdsApp.ads().withCondition("Status = ENABLED"). - Requests each URL with
UrlFetchApp.fetch()under a normal Chrome User-Agent. Apps Script fetches are synchronous, so the run time scales with the number of distinct URLs — hence the cache. - Detects HTTP 4xx client errors (404 Not Found, 410 Gone), HTTP 5xx server errors (500, 502, 503), and checks response body against regex patterns for out-of-stock messages.
- Logs timestamp, campaign, ad group, URL, and error code into a dedicated Google Sheet, optionally pauses broken ads, and sends an email summary.
Step-by-step installation
- In your Google Ads account, click Tools and Settings > Bulk Actions > Scripts.
- Click the plus (+) button to create a new script.
- Copy and paste the code above into the script editor.
- Create a new Google Sheet and paste its URL into the SPREADSHEET_URL config line.
- Click "Authorize" to grant the script access to Google Ads and Google Sheets.
- Click "Preview" to test the run, then save and set a daily schedule (e.g., 06:00 AM).
Frequently asked questions
Will this slow down my ads or my site?
It runs on Google's infrastructure, not in your account's serving path, so ads are unaffected. It does hit your site once per unique URL, which is why the code caches URLs it has already checked.
Does it check sitelinks and price assets?
Ads and keywords out of the box. Sitelinks need one more loop over SitelinkExtensions, passed through the same checkUrl().