Google Ads Script25.02.2026
Автор: Артем Мотин
Как перестать сливать бюджет на “битые” ссылки. Скрипт для Google Ads
Каждый PPC-специалист хоть раз сталкивался с ситуацией: кампания работает, бюджет тратится, а конверсий ноль. Заходишь проверить - а там страница 404 или, что еще хуже, надпись “Товара нет в наличии” при вполне рабочем коде ответа 200.
Стандартные чекеры часто смотрят только на статус-код сервера. Но современные сайты коварны: страница может открываться, но быть бесполезной для рекламы. Я решил эту проблему с помощью кастомного скрипта и сегодня поделюсь им с вами.
Почему обычного URL-чекера недостаточно?
Большинство скриптов проверяют только наличие ошибки 404. Но для эффективного PPC этого мало. Вот реальные сценарии, которые «проглатывают» стандартные инструменты:
- Мягкая 404: Страница открывается, но на ней текст “Ничего не найдено”, “Нет в наличии”, “Товар ожидается” и т.д.
- Технические работы: например, сообщение “Сервис временно недоступен”.
- Пустые категории: В интернет-магазине остался шаблон страницы, но товаров в наличии нет.
Мой скрипт работает глубже. Он анализирует содержимое страницы и ищет конкретные маркеры (стоп-фразы), которые вы задаете сами в настройках скрипта.
Что умеет этот скрипт?
- Двусторонний контроль: Скрипт не только ставит на паузу группы объявлений с плохими ссылками, но и автоматически включает их обратно, когда страница восстанавливается.
- Умный анализ контента: Перед поиском стоп-слов скрипт “вычищает” из HTML-кода лишние скрипты и стили. Это исключает ложные срабатывания, если стоп-слово встретилось где-то в техническом коде страницы. Также обрабатываются стандартные ошибки 404, 503 и т.д.
- Обход блокировок: Используется актуальный User-Agent, чтобы серверы сайта не принимали запрос скрипта за подозрительную активность бота.
- Контроль состояний: Если у вас огромный аккаунт, скрипт не упадет по таймауту. Он запоминает, на чем остановился, и продолжит проверку при следующем запуске.
- Прозрачное логирование: Все действия записываются в Google Таблицу - вы всегда видите, когда, почему и какая группа была остановлена или включена.
Инструкция по настройке скрипта
Шаг 1: Подготовка таблицы
Скопируйте на свой Google Диск таблицу и скопируйте её URL.
Шаг 2: Настройка скрипта
В коде скрипта (ниже) вам нужно отредактировать блок SETTINGS:
SPREADSHEET_URL: Вставьте ссылку на вашу таблицу (предварительно скопируйте таблицу, указанную в коде скрипта, на свой Google Диск).MARKER_TEXTS: Список фраз, при нахождении которых группу нужно остановить (например, 'Нет в наличии', 'Сервис временно недоступен' и т.д. - точный список маркеров вы формируете самостоятельно, под конкретный проект).CAMPAIGN_NAME_CONTAINS_OR: Если хотите проверять не весь аккаунт, а только конкретные кампании - впишите части их названий. Скрипт будет искать кампании по правилу “Название содержит […]”. То есть, в коде скрипта вам нужно заменить текст “Your-campaign-name-or-part-of-the-name” на часть названия кампании/кампаний. Если вы хотите, чтобы скрипт проверял все кампании в рекламном аккаунте - оставьте пустыми квадратные скобки [ ].
Шаг 3: Запуск
Установите скрипт в аккаунте Google Ads, задайте расписание (как минимум - раз в сутки) и нажмите “Выполнить”.
Код скрипта:
// Copyright 2026, Google LLC
//
// 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
// https://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.
//
// ------------------------------------------------
// Developed by: Artem Motin
// Project: Quantum — PPC Automation Workspace
// Official Website: https://www.myquantum.app
// ------------------------------------------------
//
// ------------------- SETTINGS ------------------- //
const SPREADSHEET_URL = "https://docs.google.com/spreadsheets/d/1wUS4v32Hh9Y_C-Y_NvV47k_nLwL4_fpcG-5HWVaXZ_c/edit";
const PAUSED_LOG_SHEET_NAME = "Paused Log";
const ENABLED_LOG_SHEET_NAME = "Enabled Log";
const MARKER_TEXTS = ['Немає в наявності', 'Нет в наличии', 'Out of stock'];
const CAMPAIGN_NAME_CONTAINS_OR = ["Your-campaign-name-or-part-of-the-name"];
const LABEL_NAME = "Auto-Paused by URL Checker";
// ------------------- END OF SETTINGS ------------------- //
function main() {
ensureLabelExists(LABEL_NAME);
const properties = PropertiesService.getScriptProperties();
let state = properties.getProperty('currentScriptState') || 'REENABLE';
while(AdsApp.getExecutionInfo().getRemainingTime() > 120) {
console.log(`Current state: ${state}`);
if (state === 'REENABLE') {
const result = reenableGoodAdGroups();
if (result.isFinished) {
state = 'PAUSE';
properties.setProperty('currentScriptState', 'PAUSE');
console.log("Re-enable step finished. Transitioning to Pause step in the same run.");
} else {
console.log("Re-enable step did not finish. Will resume on the next run.");
break;
}
}
if (state === 'PAUSE') {
const result = pauseBadAdGroups();
if (result.isFinished) {
state = 'FINISHED';
properties.deleteProperty('currentScriptState');
console.log("Pause step finished. All tasks completed for this cycle.");
} else {
console.log("Pause step did not finish. Will resume on the next run.");
}
break;
}
if (state === 'FINISHED') {
break;
}
}
console.log("Script execution finished for this run.");
}
function pauseBadAdGroups() {
const properties = PropertiesService.getScriptProperties();
const pausedSheet = getSheet(SPREADSHEET_URL, PAUSED_LOG_SHEET_NAME, ["Date", "Campaign", "Ad Group", "Broken URL", "Reason"]);
const urlsToAdGroups = getUrlsToAdGroupsMapping();
const urls = Object.keys(urlsToAdGroups);
const pausedAdGroupIdsInThisRun = new Set();
const startIndex = parseInt(properties.getProperty('pauseProgressIndex') || '0', 10);
if(urls.length === 0) {
console.log("No ads found to check within the specified campaigns.");
properties.deleteProperty('pauseProgressIndex');
return { isFinished: true };
}
if (startIndex === 0) console.log(`Found ${urls.length} unique URLs to check.`);
else console.log(`Resuming pause step. Starting from URL #${startIndex + 1} of ${urls.length}.`);
let i;
for (i = startIndex; i < urls.length; i++) {
if (AdsApp.getExecutionInfo().getRemainingTime() < 120) {
properties.setProperty('pauseProgressIndex', i);
return { isFinished: false };
}
const url = urls[i];
console.log(`Checking URL ${i + 1}/${urls.length}: ${url}`);
const checkResult = isUrlBroken(url);
if (checkResult.isBroken) {
const adGroupsData = urlsToAdGroups[url];
for (const adGroupData of adGroupsData) {
if (pausedAdGroupIdsInThisRun.has(adGroupData.adGroupId)) continue;
const adGroup = getAdGroupById(adGroupData.campaignId, adGroupData.adGroupId);
if (adGroup && !adGroup.isPaused()) {
adGroup.applyLabel(LABEL_NAME);
adGroup.pause();
console.log(`Ad group "${adGroup.getName()}" has been paused.`);
pausedAdGroupIdsInThisRun.add(adGroupData.adGroupId);
pausedSheet.appendRow([new Date(), adGroup.getCampaign().getName(), adGroup.getName(), url, checkResult.reason]);
}
}
}
}
if (i >= urls.length) {
console.log("All URLs checked. Resetting progress for this step.");
properties.deleteProperty('pauseProgressIndex');
return { isFinished: true };
}
return { isFinished: false };
}
function reenableGoodAdGroups() {
const properties = PropertiesService.getScriptProperties();
const enabledSheet = getSheet(SPREADSHEET_URL, ENABLED_LOG_SHEET_NAME, ["Date", "Campaign", "Ad Group", "Checked URL"]);
const labelIterator = AdsApp.labels().withCondition(`Name = '${LABEL_NAME}'`).get();
if (!labelIterator.hasNext()) {
console.log(`Label "${LABEL_NAME}" not found. Skipping re-enable step.`);
return { isFinished: true };
}
const label = labelIterator.next();
const campaignIdsToFilter = getMatchingCampaignIds();
const adGroupSelector = label.adGroups().withCondition("Status = 'PAUSED'");
if (campaignIdsToFilter.length > 0) {
adGroupSelector.withCondition(`CampaignId IN [${campaignIdsToFilter.join(',')}]`);
} else if (CAMPAIGN_NAME_CONTAINS_OR && CAMPAIGN_NAME_CONTAINS_OR.length > 0) {
// If filter is set but no campaigns match, do nothing.
console.log("No campaigns found matching the name criteria for the re-enable step. Skipping.");
return { isFinished: true };
}
const adGroupIterator = adGroupSelector.get();
const adGroupIds = [];
while(adGroupIterator.hasNext()){
adGroupIds.push(adGroupIterator.next().getId());
}
if(adGroupIds.length === 0) {
console.log("No previously paused ad groups found to check within the specified campaigns.");
properties.deleteProperty('reenableProgressIndex');
return { isFinished: true };
}
let startIndex = parseInt(properties.getProperty('reenableProgressIndex') || '0', 10);
if (startIndex >= adGroupIds.length) {
console.log(`Saved index (${startIndex}) is out of bounds (total groups: ${adGroupIds.length}). Resetting progress to 0.`);
startIndex = 0;
}
if (startIndex === 0) console.log(`Found ${adGroupIds.length} previously paused ad groups to check.`);
else console.log(`Resuming re-enable step. Starting from ad group #${startIndex + 1} of ${adGroupIds.length}.`);
let i;
for (i = startIndex; i < adGroupIds.length; i++) {
if (AdsApp.getExecutionInfo().getRemainingTime() < 120) {
properties.setProperty('reenableProgressIndex', i);
return { isFinished: false };
}
const adGroup = AdsApp.adGroups().withIds([adGroupIds[i]]).get().next();
console.log(`Checking paused group ${i+1}/${adGroupIds.length}: "${adGroup.getName()}"`);
const ads = adGroup.ads().withCondition("Status IN ['ENABLED', 'PAUSED']").get();
if (!ads.hasNext()) {
adGroup.enable();
safelyRemoveLabel(adGroup, LABEL_NAME);
console.log(`Ad group "${adGroup.getName()}" has been enabled (it contained no ads to check).`);
continue;
}
let isSafeToEnable = true;
let lastCheckedUrl = "";
const urlsInGroup = new Set();
while(ads.hasNext()){
const ad = ads.next();
if(ad.urls().getFinalUrl()) urlsInGroup.add(ad.urls().getFinalUrl());
}
for (const url of urlsInGroup) {
lastCheckedUrl = url;
const checkResult = isUrlBroken(url);
if (checkResult.isBroken) {
// FIX: Логируем причину отказа
console.log(`[SKIP] Group "${adGroup.getName()}" NOT enabled. URL: ${url} | Reason: ${checkResult.reason}`);
isSafeToEnable = false;
break;
}
}
if (isSafeToEnable) {
adGroup.enable();
safelyRemoveLabel(adGroup, LABEL_NAME);
console.log(`Ad group "${adGroup.getName()}" has been re-enabled.`);
enabledSheet.appendRow([new Date(), adGroup.getCampaign().getName(), adGroup.getName(), lastCheckedUrl || "N/A"]);
}
}
if(i >= adGroupIds.length){
console.log("All paused groups checked. Resetting progress for this step.");
properties.deleteProperty('reenableProgressIndex');
return { isFinished: true };
}
return { isFinished: false };
}
// --- Helper Functions ---
function isUrlBroken(url) {
const RETRY_COUNT = 3;
const RETRY_DELAY_MS = 2000;
for (let i = 0; i < RETRY_COUNT; i++) {
try {
if (i > 0) {
console.log(`Retrying URL (${i + 1}/${RETRY_COUNT}): ${url}`);
Utilities.sleep(RETRY_DELAY_MS);
}
const response = UrlFetchApp.fetch(url, {
muteHttpExceptions: 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'
}
});
const responseCode = response.getResponseCode();
if (responseCode >= 400 && responseCode < 500) return { isBroken: true, reason: `HTTP Status ${responseCode}` };
if (responseCode >= 500) {
if (i < RETRY_COUNT - 1) continue;
else return { isBroken: true, reason: `HTTP Status ${responseCode} (after ${RETRY_COUNT} attempts)` };
}
let rawContent = response.getContentText();
let cleanContent = rawContent.replace(/<script[^>]*>([\s\S]*?)<\/script>/gi, " ");
cleanContent = cleanContent.replace(/<style[^>]*>([\s\S]*?)<\/style>/gi, " ");
const contentLowerCase = cleanContent.toLowerCase();
for (const marker of MARKER_TEXTS) {
if (contentLowerCase.includes(marker.toLowerCase())) {
return { isBroken: true, reason: `Found text: "${marker}"` };
}
}
return { isBroken: false, reason: '' };
} catch (e) {
if (i < RETRY_COUNT - 1) continue;
return { isBroken: true, reason: `URL fetch error: ${e.message}` };
}
}
}
function safelyRemoveLabel(entity, labelName) {
const labelIterator = entity.labels().withCondition(`Name = '${labelName}'`).get();
if (labelIterator.hasNext()) entity.removeLabel(labelName);
else console.warn(`Attempted to remove label "${labelName}" from "${entity.getName()}", but it was not found.`);
}
function getMatchingCampaignIds() {
const campaignIds = [];
if (typeof CAMPAIGN_NAME_CONTAINS_OR !== 'undefined' && CAMPAIGN_NAME_CONTAINS_OR && CAMPAIGN_NAME_CONTAINS_OR.length > 0) {
const campaignIterator = AdsApp.campaigns()
.withCondition("Status = 'ENABLED'")
.get();
while (campaignIterator.hasNext()) {
const campaign = campaignIterator.next();
const campaignName = campaign.getName();
const matches = CAMPAIGN_NAME_CONTAINS_OR.some(namePart => campaignName.includes(namePart));
if (matches) {
campaignIds.push(campaign.getId());
}
}
}
return campaignIds;
}
function getUrlsToAdGroupsMapping() {
const mapping = {};
const campaignIdsToFilter = getMatchingCampaignIds();
if (CAMPAIGN_NAME_CONTAINS_OR && CAMPAIGN_NAME_CONTAINS_OR.length > 0 && campaignIdsToFilter.length === 0) {
console.log("No campaigns found matching the name criteria. Skipping ad check.");
return {};
}
const adSelector = AdsApp.ads()
.withCondition("Status IN ['ENABLED', 'PAUSED']")
.withCondition("AdGroupStatus = 'ENABLED'")
.withCondition("CampaignStatus = 'ENABLED'");
if (campaignIdsToFilter.length > 0) {
adSelector.withCondition(`CampaignId IN [${campaignIdsToFilter.join(',')}]`);
}
const ads = adSelector.get();
while (ads.hasNext()) {
const ad = ads.next();
const url = ad.urls().getFinalUrl();
if (url) {
if (!mapping[url]) mapping[url] = [];
const adGroup = ad.getAdGroup();
if (!mapping[url].some(item => item.adGroupId === adGroup.getId())) {
mapping[url].push({adGroupId: adGroup.getId(), campaignId: adGroup.getCampaign().getId()});
}
}
}
return mapping;
}
function getSheet(spreadsheetUrl, sheetName, headers) {
const spreadsheet = SpreadsheetApp.openByUrl(spreadsheetUrl);
let sheet = spreadsheet.getSheetByName(sheetName);
if (!sheet) {
sheet = spreadsheet.insertSheet(sheetName);
if (headers && headers.length > 0) sheet.appendRow(headers);
}
return sheet;
}
function ensureLabelExists(labelName) {
if (!AdsApp.labels().withCondition(`Name = '${labelName}'`).get().hasNext()) {
AdsApp.createLabel(labelName, "For ad groups paused by the URL checker script");
console.log(`Created label: "${labelName}"`);
}
}
function getAdGroupById(campaignId, adGroupId) {
const iterator = AdsApp.adGroups().withCondition(`CampaignId = ${campaignId}`).withCondition(`Id = ${adGroupId}`).get();
if (iterator.hasNext()) return iterator.next();
return null;
}
Заключение
Этот скрипт - ваш личный “ночной сторож”, который экономит часы ручной проверки и сотни долларов клиентского бюджета. Он простой, надежный и делает именно то, что нужно практику.
Если вам близок такой подход к автоматизации и вы хотите еще сильнее упростить работу с Google Ads - попробуйте Quantum. Это SaaS-сервис, который я развиваю, чтобы PPC-специалисты могли генерировать готовые кампании почти автоматически, не тратя время на рутину.
Есть вопросы по работе скрипта? Пишите в комментариях к посту в нашем Telegram-канале, разберемся вместе!