Робимо інструмент перевірки аптайму сайтів з сповіщеннями у Telegram
Зміст
- Таблиця з переліком сайтів для перевірки
- Власний чат-бот в Telegram
- Скрипт перевірки
- Налаштовуємо періодичність виконання скрипта
Був в мене клієнт в якого постійно лягав сайт, через що частина бюджету на рекламу уходила в нікуди. Звісно, гугл рано чи пізно відхиляє оголошення, що ведуть на непрацюючий сайт, але робить він це за дивною логікою і не дуже оперативно + не в усіх форматах реклами
Тоді я знайшов безкоштовний аптайм чекер, додав туди 1 сайт і забув про цю проблему, поки в мене не з’явився інший клієнт з постійно падаючим сайтом
Оскільки безкоштовний тариф більшості сервісів дозволяє додати лише 1 сайт, виникла ідея зробити рішення, яке буде автоматично перевіряти велику кількість сайтів за логікою:
додали пару десятків сайтів в гугл док > скрипт перевіряє всі сайти зі списку кожні N-хвилин > якщо знайшов помилку завантаження сторінки – відправляє сповіщення у телеграм
Власне саме таке рішення ми і будемо зараз розгортати
1. Таблиця з переліком сайтів
Спочатку нам потрібно створити просту гугл таблицю з двома вкладками “Сайти” та “Лог”
Тут важливо зберегти порядок та назви вкладок, вони зашиті у скрипт, назва самого документу не принципова. Можете просто скопіювати собі готовий шаблон
На даному етапі нам потрібно скопіювати ID саме вашої копії документу – частину адреси сторінки що виділено жирним
docs.google.com/spreadsheets/d/ 1czRW1o2… /edit
2. Власний чат-бот в Telegram
Щоб отримувати сповіщення потрібно створити свій окремий бот в який ви будете отримувати всі сповіщення скрипта
До речі, цей бот можна буде використовувати і для інших сповіщень, від інших скриптів, наприклад, я отримую туди сповіщення про низький баланс на акаунтах, або міні-звіти по проектах
- В пошуку по контактах вводимо @BotFather
- Тиснемо /start, потім /newbot
- Вводимо ім’я/назву бота
- Вводимо унікальний та ніким не зайнятий юзернейм бота, який закінчується на bot (наприклад, My_Bot)
- Чат видає нам токен — він виглядає як 12345678910:ABCD… — копіюємо його кудись для наступного етапу
Коли ми створили бота потрібно дізнатись ID чату:
- Запускаємо свого бота прописавши /start
- В рядок браузера вставляємо api.telegram.org/bot 12345678910:ABCD… /getUpdates
- Браузер видасть текст де буде частина “chat”: { “id”: 123456789, …} — ось ці цифри і будуть id чату — зберігаємо їх для наступного кроку
3. Скрипт перевірки
- Переходимо на сторінку Google Apps Script
- Вставляємо у поле готовий текст скрипту замінивші три змінні на самому початку коду:
const TELEGRAM_TOKEN = ‘12345678910:ABCD…‘;
const CHAT_ID = ‘123456789‘;
const SPREADSHEET_ID = ‘123abcdefg‘;
Сам код скрипта:
// Токен телеграм бота
const TELEGRAM_TOKEN = '12345:ABCDF';
// ID чату, куди будуть приходити сповіщення від бота
const CHAT_ID = '12345678';
// ID гугл таблиці
const SPREADSHEET_ID = '123abcdefg';
// Назва аркуша, де зберігаються перевірювані сайти
const SITES_SHEET_NAME = 'Сайти';
// Назва аркуша для збереження журналу подій (збої, відновлення тощо)
const LOG_SHEET_NAME = 'Лог';
// Кількість сайтів, що перевіряються за один пакет (щоб не перевантажувати запитами)
const BATCH_SIZE = 50;
// Максимальний час очікування відповіді від сайту (в мс)
const FETCH_TIMEOUT_MS = 15000;
// Поріг часу (мс), після якого сайт вважається "повільним"
const SLOW_RESPONSE_THRESHOLD_MS = 10000;
function checkWebsites() {
Logger.log(`Запуск checkWebsites: ${new Date()}`); // Лог старту функції
const startTime = new Date(); // Час початку виконання
const spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID); // Відкриття Google Таблиці за ID
// ===== Ініціалізація аркуша "Сайти" =====
let sitesSheet = spreadsheet.getSheetByName(SITES_SHEET_NAME);
if (!sitesSheet) {
// Якщо аркуш не існує — створюємо і додаємо заголовки колонок
sitesSheet = spreadsheet.insertSheet(SITES_SHEET_NAME);
sitesSheet.appendRow(['URL', 'Код відповіді', 'Час перевірки', 'Статус', 'Пояснення']);
}
// ===== Ініціалізація аркуша "Лог" =====
let logSheet = spreadsheet.getSheetByName(LOG_SHEET_NAME);
if (!logSheet) {
logSheet = spreadsheet.insertSheet(LOG_SHEET_NAME);
logSheet.appendRow(['Час', 'URL', 'Статус', 'Код відповіді', 'Пояснення']);
}
// Отримуємо список URL з колонки A (починаючи з рядка 2)
const values = sitesSheet.getRange('A2:A').getValues();
const urls = []; // Масив об'єктів для fetchAll
const indices = []; // Індекси відповідних рядків у таблиці
// Проходимо по кожному значенню в колонці A
for (let i = 0; i < values.length; i++) {
let url = values[i][0];
if (!url) continue; // Пропускаємо пусті рядки
url = normalizeUrl(url); // Додаємо https:// якщо немає
// Готуємо параметри для UrlFetchApp
urls.push({ url: url, muteHttpExceptions: true, timeout: FETCH_TIMEOUT_MS });
indices.push(i); // Запам'ятовуємо індекс
}
// Якщо немає жодного URL для перевірки — вихід
if (urls.length === 0) {
Logger.log('Немає URL для перевірки');
return;
}
const slowUrls = []; // Список повільних сайтів для повторної перевірки
// ===== Перевірка сайтів пакетами =====
for (let batchStart = 0; batchStart < urls.length; batchStart += BATCH_SIZE) {
// Формуємо поточний пакет URL
const batchUrls = urls.slice(batchStart, batchStart + BATCH_SIZE);
const batchIndices = indices.slice(batchStart, batchStart + BATCH_SIZE);
const timestamp = new Date();
const formattedTime = Utilities.formatDate(timestamp, 'Europe/Kiev', 'dd.MM.yyyy HH:mm:ss');
let responses = [];
try {
Logger.log(`Виконання fetchAll для партії ${batchStart / BATCH_SIZE + 1}`);
// Одночасно отримуємо відповіді з усіх сайтів у пакеті
responses = UrlFetchApp.fetchAll(batchUrls);
} catch (error) {
// Якщо fetchAll дає помилку — перевіряємо кожен сайт індивідуально
Logger.log(`Помилка fetchAll: ${error}`);
for (let j = 0; j < batchUrls.length; j++) {
try {
responses[j] = UrlFetchApp.fetch(batchUrls[j].url, {
muteHttpExceptions: true,
timeout: FETCH_TIMEOUT_MS
});
} catch (e) {
Logger.log(`Помилка для ${batchUrls[j].url}: ${e}`);
responses[j] = null; // Якщо теж не вдалося
}
}
}
// ===== Обробка відповідей =====
for (let j = 0; j < responses.length; j++) {
const i = batchIndices[j]; // Індекс рядка у таблиці
let url = batchUrls[j].url;
let code, explanation;
const start = new Date(); // Час початку вимірювання швидкості відповіді
// Якщо відповіді немає — пробуємо HTTP замість HTTPS
if (!responses[j]) {
slowUrls.push(url); // Додаємо в список "проблемних"
url = url.replace('https://', 'http://');
try {
const fallbackResponse = UrlFetchApp.fetch(url, { muteHttpExceptions: true, timeout: FETCH_TIMEOUT_MS });
responses[j] = fallbackResponse;
} catch (error) {
Logger.log(`Помилка для ${url} (http): ${error}`);
}
}
// Обробка відповіді (отримання коду, пояснення, замір часу)
try {
if (responses[j]) {
code = responses[j].getResponseCode();
explanation = getStatusExplanation(code);
const responseTime = new Date() - start;
if (responseTime > SLOW_RESPONSE_THRESHOLD_MS) {
slowUrls.push(url); // Якщо відповідь надто довга — додаємо в повільні
}
} else {
code = '❌';
explanation = 'Помилка запиту (адреса недоступна)';
}
} catch (error) {
code = '❌';
explanation = 'Помилка запиту (адреса недоступна)';
Logger.log(`Помилка обробки відповіді для ${url}: ${error}`);
}
// Отримуємо попередній код відповіді, щоб визначити зміну статусу
const previousCode = sitesSheet.getRange(i + 2, 2).getValue() || 200;
// Запис результатів у таблицю "Сайти"
try {
sitesSheet.getRange(i + 2, 2).setValue(code); // Код відповіді
sitesSheet.getRange(i + 2, 3).setValue(formattedTime); // Час перевірки
sitesSheet.getRange(i + 2, 4).setValue(code === 200 ? '✅ Працює' : '⚠️ Проблема'); // Статус
sitesSheet.getRange(i + 2, 5).setValue(explanation); // Пояснення
} catch (error) {
Logger.log(`Помилка запису в аркуш Сайти для ${url}: ${error}`);
}
// ===== Логіка відправки сповіщень =====
if (code !== 200 && previousCode === 200) {
// Сайт впав — надсилаємо повідомлення про збій
sendTelegramMessage(`❗ Збій сайту:\n${url}\nЧас: ${formattedTime}\nКод: ${code}\nПояснення: ${explanation} (${code})`);
logSheet.appendRow([formattedTime, url, 'Збій', code, explanation]);
} else if (code === 200 && previousCode !== 200) {
// Сайт відновився — надсилаємо повідомлення
sendTelegramMessage(`✅ Сайт відновлено:\n${url}\nЧас: ${formattedTime}`);
logSheet.appendRow([formattedTime, url, 'Відновлення', code, 'Сайт знову працює']);
}
}
// Якщо є ще пакети для перевірки — робимо паузу, щоб не навантажувати систему
if (batchStart + BATCH_SIZE < urls.length) {
Utilities.sleep(1000);
}
}
// ===== Повторна перевірка повільних сайтів =====
if (slowUrls.length > 0) {
Logger.log(`⚠️ Починаємо повторну перевірку повільних сайтів (${slowUrls.length})`);
retrySlowUrls(slowUrls);
}
Logger.log(`Завершення checkWebsites: тривалість ${(new Date() - startTime) / 1000} секунд`);
}
// ===== Повторна перевірка повільних сайтів =====
function retrySlowUrls(urls) {
const formattedTime = Utilities.formatDate(new Date(), 'Europe/Kiev', 'dd.MM.yyyy HH:mm:ss');
for (let url of urls) {
try {
const response = UrlFetchApp.fetch(url, { muteHttpExceptions: true, timeout: 20000 });
const code = response.getResponseCode();
const explanation = getStatusExplanation(code);
Logger.log(`🔁 Повторна перевірка ${url}: ${code} (${explanation})`);
} catch (error) {
Logger.log(`❌ Повторна перевірка не вдалася для ${url}: ${error}`);
}
Utilities.sleep(300); // Невелика пауза між перевірками
}
}
// ===== Нормалізація URL (додає https://, якщо немає) =====
function normalizeUrl(url) {
url = url.trim();
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
return url;
}
// ===== Пояснення кодів HTTP =====
function getStatusExplanation(code) {
const explanations = {
200: 'Успішно',
301: 'Постійне перенаправлення',
302: 'Тимчасове перенаправлення',
400: 'Неправильний запит',
401: 'Неавторизовано',
403: 'Заборонено (немає доступу)',
404: 'Сторінку не знайдено',
408: 'Час запиту вичерпано',
410: 'Сторінку видалено',
429: 'Забагато запитів',
500: 'Внутрішня помилка сервера',
502: 'Помилка шлюзу (Bad Gateway)',
503: 'Сервер тимчасово недоступний',
504: 'Час очікування шлюзу вичерпано'
};
return explanations[code] || 'Невідома помилка';
}
// ===== Надсилання повідомлення в Telegram =====
function sendTelegramMessage(message) {
const url = `https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage`;
const payload = {
chat_id: CHAT_ID,
text: message,
parse_mode: 'HTML'
};
try {
UrlFetchApp.fetch(url, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
} catch (error) {
Logger.log(`Помилка надсилання повідомлення в Telegram: ${error}`);
}
}
// ===== Налаштування тригера для автоматичного запуску скрипта =====
function setTrigger() {
Logger.log('Налаштування тригера');
const triggers = ScriptApp.getProjectTriggers();
// Видаляємо старий тригер, якщо він існує
for (const trigger of triggers) {
if (trigger.getHandlerFunction() === 'checkWebsites') {
ScriptApp.deleteTrigger(trigger);
Logger.log('Видалено старий тригер');
}
}
// Створюємо новий тригер на виконання кожні 10 хвилин
try {
ScriptApp.newTrigger('checkWebsites')
.timeBased()
.everyMinutes(10)
.create();
Logger.log('Тригер створено');
} catch (error) {
Logger.log(`Помилка створення тригера: ${error}`);
}
}
- Зберігаємо скрипт на гугл диску через кнопку іконки з дискетою трохи вище коду
- Активуємо скрипт синьою кнопкою “Ввести в дію” в правому верхньому куті, далі всюди тиснемо “Ок”
4. Налаштовуємо періодичність виконання скрипта
- В бічній панелі праворуч обираємо пункт “Тригери” тобто умови виконання скрипта
- В правому нижньому куті тиснемо синю кнопку “Додати тригер” і обираємо періодичність виконання скрипта, оптимальний варіант не частіше 10 хвилин + обираємо, щоб сервіс негайно сповіщав вас про помилки виконання, ці сповіщення будуть вже не у телеграм, а на пошту

Все, готово) Для перевірки додайте в гугл док посилання на неіснуючий або неробочій сайт, після чого або зачекайте спрацювання тригера, або запустіть скрипт вручну, в Apps Script є кнопка play для запуску скрипта в редакторі
Під самим редактором можна побачити лог з помилками, якщо вони будуть