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-фахівці могли генерувати готові кампанії майже автоматично за допомогою AI, не витрачаючи час на рутину.
Є питання по роботі скрипта? Пишіть в коментарях до посту в нашому Telegram-каналі, розберемось разом!