Як перестати зливати бюджет на “биті” посилання: Скрипт автоматичного моніторинга URL в 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, задайте розклад (як мінімум - раз на добу) і натисніть “Виконати”.
Код скрипта:
javascript// 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']; /** * (Optional) To make the script run only on specific campaigns. * The script will check campaigns where the name contains ANY of the phrases below. * To specify multiple conditions: ["Search", "Brand", "Shopping"] * To specify a single condition: ["Search"] * To check all campaigns, leave the array empty: [] */ const CAMPAIGN_NAME_CONTAINS_OR = ["Your-campaign-name-or-part-of-the-name"]; const LABEL_NAME = "Auto-Paused by URL Checker"; // ------------------- END OF SETTINGS ------------------- // /** * Main function that orchestrates the script's execution using a state machine. * It processes tasks sequentially and saves progress to handle timeouts. */ 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 --- /** * Checks if a given URL is broken, with a retry mechanism for server errors (5xx). * @param {string} url - The URL to check. * @return {{isBroken: boolean, reason: string}} - An object with the check result. */ 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.`); } /** * Gets a list of campaign IDs that match the names in the settings. * @return {string[]} - An array of campaign IDs. */ 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-каналі, розберемось разом!
