uk
Google AdsWeb & UI/UX

Робимо інструмент перевірки аптайму сайтів з сповіщеннями у Telegram

хвилин

Зміст

  1. Таблиця з переліком сайтів для перевірки
  2. Власний чат-бот в Telegram
  3. Скрипт перевірки
  4. Налаштовуємо періодичність виконання скрипта

Був в мене клієнт в якого постійно лягав сайт, через що частина бюджету на рекламу уходила в нікуди. Звісно, гугл рано чи пізно відхиляє оголошення, що ведуть на непрацюючий сайт, але робить він це за дивною логікою і не дуже оперативно + не в усіх форматах реклами

Тоді я знайшов безкоштовний аптайм чекер, додав туди 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 TELEGRAM_TOKEN = '12345:ABCDF';
const CHAT_ID = '12345678';

// Назви аркушів
const SITES_SHEET_NAME = 'Сайти';
const LOG_SHEET_NAME = 'Лог';

// Максимальна кількість сайтів у партії
const BATCH_SIZE = 50;

function checkWebsites() {
  Logger.log(`Запуск checkWebsites: ${new Date()}`);
  const startTime = new Date();
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  
  // Отримуємо аркуш із сайтами
  let sitesSheet;
  try {
    sitesSheet = spreadsheet.getSheetByName(SITES_SHEET_NAME);
    if (!sitesSheet) {
      sitesSheet = spreadsheet.insertSheet(SITES_SHEET_NAME);
      sitesSheet.appendRow(['URL', 'Код відповіді', 'Час перевірки', 'Статус', 'Пояснення']);
    }
  } catch (error) {
    Logger.log(`Помилка доступу до аркуша Сайти: ${error}`);
    return;
  }
  
  // Отримуємо аркуш для логів
  let logSheet;
  try {
    logSheet = spreadsheet.getSheetByName(LOG_SHEET_NAME);
    if (!logSheet) {
      logSheet = spreadsheet.insertSheet(LOG_SHEET_NAME);
      logSheet.appendRow(['Час', 'URL', 'Статус', 'Код відповіді', 'Пояснення']);
    }
  } catch (error) {
    Logger.log(`Помилка доступу до аркуша Лог: ${error}`);
    return;
  }

  let values;
  try {
    values = sitesSheet.getRange('A2:A').getValues();
  } catch (error) {
    Logger.log(`Помилка читання URL: ${error}`);
    return;
  }

  const urls = [];
  const indices = [];
  
  // Збираємо непорожні URL
  for (let i = 0; i < values.length; i++) {
    let url = values[i][0];
    if (!url) continue;
    url = normalizeUrl(url);
    urls.push({ url: url, muteHttpExceptions: true });
    indices.push(i);
  }

  if (urls.length === 0) {
    Logger.log('Немає URL для перевірки');
    return;
  }

  if (urls.length > 60) {
    Logger.log(`Попередження: Кількість сайтів (${urls.length}) перевищує рекомендований ліміт 60`);
  }

  // Обробляємо URL партіями
  for (let batchStart = 0; batchStart < urls.length; batchStart += BATCH_SIZE) {
    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) {
      Logger.log(`Помилка fetchAll: ${error}`);
      // Перевіряємо кожен URL окремо
      for (let j = 0; j < batchUrls.length; j++) {
        try {
          responses[j] = UrlFetchApp.fetch(batchUrls[j].url, { muteHttpExceptions: true });
        } 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;

      // Спроба з http://
      if (!responses[j]) {
        url = url.replace('https://', 'http://');
        try {
          const fallbackResponse = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
          responses[j] = fallbackResponse;
        } catch (error) {
          Logger.log(`Помилка для ${url} (http): ${error}`);
        }
      }

      try {
        if (responses[j]) {
          code = responses[j].getResponseCode();
          explanation = getStatusExplanation(code);
        } 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) {
        const message = `❗ Збій сайту:\n${url}\nЧас: ${formattedTime}\nКод: ${code}\nПояснення: ${explanation} (${code})`;
        sendTelegramMessage(message);
        try {
          logSheet.appendRow([formattedTime, url, 'Збій', code, explanation]);
        } catch (error) {
          Logger.log(`Помилка запису в аркуш Лог: ${error}`);
        }
      }
      // Логування відновлення
      else if (code === 200 && previousCode !== 200) {
        const message = `✅ Сайт відновлено:\n${url}\nЧас: ${formattedTime}`;
        sendTelegramMessage(message);
        try {
          logSheet.appendRow([formattedTime, url, 'Відновлення', code, 'Сайт знову працює']);
        } catch (error) {
          Logger.log(`Помилка запису в аркуш Лог: ${error}`);
        }
      }
    }

    // Затримка між партіями
    if (batchStart + BATCH_SIZE < urls.length) {
      Utilities.sleep(1000);
    }
  }

  Logger.log(`Завершення checkWebsites: тривалість ${(new Date() - startTime) / 1000} секунд`);
}

// Функція для нормалізації URL
function normalizeUrl(url) {
  url = url.trim();
  if (!url.startsWith('http://') && !url.startsWith('https://')) {
    url = 'https://' + url;
  }
  return url;
}

function getStatusExplanation(code) {
  const explanations = {
    200: 'Успішно',
    301: 'Постійне перенаправлення',
    302: 'Тимчасове перенаправлення',
    400: 'Неправильний запит',
    401: 'Неавторизовано',
    403: 'Заборонено (немає доступу)',
    404: 'Сторінку не знайдено',
    408: 'Час запиту вичерпано',
    410: 'Сторінку видалено',
    429: 'Забагато запитів',
    500: 'Внутрішня помилка сервера',
    502: 'Помилка шлюзу (Bad Gateway)',
    503: 'Сервер тимчасово недоступний',
    504: 'Час очікування шлюзу вичерпано'
  };
  return explanations[code] || 'Невідома помилка';
}

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('Видалено старий тригер');
    }
  }
  try {
    ScriptApp.newTrigger('checkWebsites')
      .timeBased()
      .everyMinutes(10)
      .create();
    Logger.log('Тригер створено');
  } catch (error) {
    Logger.log(`Помилка створення тригера: ${error}`);
  }
}
  • Зберігаємо скрипт на гугл диску через кнопку іконки з дискетою трохи вище коду
  • Активуємо скрипт синьою кнопкою “Ввести в дію” в правому верхньому куті, далі всюди тиснемо “Ок”

4. Налаштовуємо періодичність виконання скрипта

  • В бічній панелі праворуч обираємо пункт “Тригери” тобто умови виконання скрипта
  • В правому нижньому куті тиснемо синю кнопку “Додати тригер” і обираємо періодичність виконання скрипта, оптимальний варіант не частіше 10 хвилин + обираємо, щоб сервіс негайно сповіщав вас про помилки виконання, ці сповіщення будуть вже не у телеграм, а на пошту

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

Під самим редактором можна побачити лог з помилками, якщо вони будуть

Попередня стаття