Google Sheets i Apps Script: monitoring pozycji bez opłat

Płatny rank tracker to wygodne narzędzie, ale przy kilkuset frazach potrafi kosztować więcej niż cały hosting. Tymczasem dane o pozycjach, kliknięciach i wyświetleniach masz już za darmo w Google Search Console. Brakuje tylko miejsca, w którym będą się odkładać w czasie, bo interfejs GSC pokazuje ruchome okno 16 miesięcy i nie zapamiętuje tego, jak wyglądał Twój wykres w zeszły wtorek. Google Sheets w połączeniu z Apps Script zamyka tę lukę: arkusz staje się archiwum, a skrypt robotem, który dopisuje do niego świeże wiersze każdej nocy.

Poniżej znajdziesz kompletny schemat takiego monitoringu: układ zakładek, kod pobierający dane, harmonogram i alert mailowy. Całość działa na darmowym koncie Google i nie wymaga serwera.

Co da się zbudować w samym arkuszu

Zanim napiszesz pierwszą linijkę kodu, ustal granicę możliwości. Arkusz nie zastąpi narzędzia, które odpytuje wyszukiwarkę z konkretnej lokalizacji i pokazuje pełny SERP z konkurencją. Z Search Console dostajesz średnią ważoną pozycję Twojej witryny dla danego zapytania, policzoną na podstawie faktycznych wyświetleń. Do monitoringu własnych treści to dane bogatsze niż scrapowany ranking, bo uwzględniają personalizację, urządzenia i kraje.

W praktyce w samym arkuszu zbudujesz:

  • historyczne archiwum par zapytanie plus strona, sięgające dalej niż 16 miesięcy interfejsu GSC,
  • dzienny snapshot pozycji, kliknięć, wyświetleń i CTR dla wybranego zbioru fraz,
  • porównanie okresów (ostatnie 7 dni kontra poprzednie 7 dni) liczone formułami, bez eksportów do CSV,
  • listę stron, które straciły pozycje lub kliknięcia, posortowaną według skali straty,
  • alert mailowy wyzwalany progiem, na przykład spadkiem o więcej niż trzy pozycje,
  • prosty wykres trendu dla frazy, wklejany do raportu dla klienta.

Czego nie zbudujesz: podglądu pozycji konkurencji, danych sprzed podpięcia witryny do GSC oraz pozycji dla fraz, na które Twoja strona nie miała ani jednego wyświetlenia. Search Console raportuje wyłącznie zapytania, w których faktycznie się pojawiłeś, więc arkusz monitoruje stan posiadania, a nie cały rynek. Osobna sprawa to widoczność w odpowiedziach modeli językowych, której GSC nie mierzy wcale; tam potrzebne są darmowe narzędzia do monitoringu cytowań AI, działające na zupełnie innej zasadzie.

Połączenie z danymi Search Console

Apps Script sięga do Search Console przez usługę zaawansowaną, czyli gotowy wrapper na Search Console API. Włączasz ją raz, w edytorze skryptu, w sekcji Services, wybierając pozycję Search Console API i akceptując domyślny identyfikator searchconsole. Od tego momentu w kodzie masz obiekt SearchConsole i nie musisz ręcznie budować żądań OAuth ani odświeżać tokenów, bo skrypt działa na uprawnieniach konta, które go uruchamia.

Warunek jest jeden: konto Google musi mieć dostęp do usługi w Search Console, minimum jako ograniczony użytkownik. Na koncie agencyjnym poproś klienta o dodanie właśnie tego adresu, a nie skrzynki, z której nigdy nie uruchomisz skryptu. Metody i parametry opisuje dokumentacja Search Analytics API.

Kluczowe pojęcie to wymiary. Odpytując API, deklarujesz, po czym grupować dane: po dacie, zapytaniu, stronie, kraju, urządzeniu. Każdy dodany wymiar rozbija te same kliknięcia na drobniejsze wiersze, więc suma po zapytaniach nigdy nie zgodzi się idealnie z sumą dzienną. To nie błąd skryptu, tylko efekt anonimizacji rzadkich zapytań.

Zdecyduj z góry, czy zbierasz dane w podziale na kraje. Przy wyświetleniach spoza Polski mieszany zbiór generuje fałszywy alarm: pozycja spada, bo urósł udział rynku, na którym rankujesz słabo, a nie dlatego, że cokolwiek straciłeś. Filtr country równy pol kosztuje jedną linijkę, a oszczędza godzinę diagnozy.

Struktura arkusza: dane, agregaty, widok

Najczęstszy błąd to jeden arkusz, w którym skrypt dopisuje dane, ktoś ręcznie sortuje, a obok stoją formuły. Po miesiącu taki plik psuje się przy każdej edycji. Rozdziel warstwy na trzy zakładki.

Zakładka Rola Kto zapisuje
raw Surowe wiersze z API, tylko dopisywanie, bez sortowania i bez formuł skrypt
agg Agregaty okresowe, porównania tydzień do tygodnia, delta pozycji formuły
view Widok dla człowieka: filtry, formatowanie warunkowe, wykresy człowiek

W zakładce raw ustal stały układ kolumn: data pomiaru, data danych, zapytanie, strona, kliknięcia, wyświetlenia, CTR, pozycja. Dwie osobne daty są istotne, bo dane w GSC dojrzewają jeszcze przez dwa do trzech dni po fakcie i ten sam dzień odpytany w poniedziałek i w czwartek zwróci inne liczby. Bez kolumny z datą pobrania nie odtworzysz później, skąd wzięła się rozbieżność.

W zakładce agg licz wszystko formułami tablicowymi na zakresie raw, przez QUERY lub SUMIFS. Minimalny zestaw to średnia pozycja z ostatnich siedmiu dni, ta sama średnia z poprzedniego tygodnia i różnica między nimi, plus suma kliknięć w obu oknach. Pozycja bez wolumenu myli: awans z 40 na 30 miejsce nie zmienia nic w ruchu, a spadek z 3 na 5 zabiera połowę kliknięć.

Jeśli chcesz z tego arkusza wyciągać nie tylko trend, ale i wnioski o intencji użytkowników, uzupełnij go o filtry wzorcowe. Sposoby budowania takich reguł opisałem w tekście o tym, jak działa regex w Search Console przy wyławianiu pytań; te same wyrażenia można wpisać do formuły w kolumnie pomocniczej i oznaczyć nimi frazy pytające.

Skrypt pobierający dane cyklicznie

Poniższy skrypt pobiera dane dla wybranego okna czasowego i dopisuje je na koniec zakładki raw. Wstaw go w edytorze (Rozszerzenia, następnie Apps Script), podmieniając adres usługi na własny. Dla witryny zweryfikowanej jako właściciel domeny użyj formatu sc-domain:twojadomena.pl, dla prefiksu URL pełnego adresu z protokołem.

const SITE = 'sc-domain:twojadomena.pl';
const SHEET = 'raw';
const ROW_LIMIT = 25000;

function pobierzDane() {
  const koniec = przesunDate(3);   // dane sprzed 3 dni sa juz dojrzale
  const start = przesunDate(9);    // okno 7 dni
  const arkusz = SpreadsheetApp.getActive().getSheetByName(SHEET);
  const pobrano = Utilities.formatDate(new Date(), 'Europe/Warsaw', 'yyyy-MM-dd');

  let wiersze = [];
  let offset = 0;

  while (true) {
    const odp = SearchConsole.Searchanalytics.query({
      startDate: start,
      endDate: koniec,
      dimensions: ['date', 'query', 'page'],
      dimensionFilterGroups: [{
        filters: [{ dimension: 'country', operator: 'equals', expression: 'pol' }]
      }],
      rowLimit: ROW_LIMIT,
      startRow: offset
    }, SITE);

    const dane = odp.rows || [];
    dane.forEach(function (r) {
      wiersze.push([
        pobrano, r.keys[0], r.keys[1], r.keys[2],
        r.clicks, r.impressions, r.ctr, r.position
      ]);
    });

    if (dane.length < ROW_LIMIT) break;
    offset += ROW_LIMIT;
  }

  if (!wiersze.length) return;
  arkusz.getRange(arkusz.getLastRow() + 1, 1, wiersze.length, 8).setValues(wiersze);
  Logger.log('Dopisano wierszy: ' + wiersze.length);
}

function przesunDate(dni) {
  const d = new Date();
  d.setDate(d.getDate()-dni);
  return Utilities.formatDate(d, 'Europe/Warsaw', 'yyyy-MM-dd');
}

Trzy decyzje w tym kodzie warto rozumieć, bo to one decydują o jakości archiwum. Po pierwsze, przesunięcie o trzy dni: odpytywanie wczorajszej daty daje wiersze, które Google jeszcze uzupełnia, więc archiwum zapełnia się zaniżonymi liczbami. Po drugie, pętla z startRow: pojedyncze żądanie zwraca maksymalnie 25 000 wierszy, a witryna z długim ogonem fraz przekroczy ten próg bez ostrzeżenia i po cichu urwie dane. Po trzecie, zapis jednym wywołaniem setValues zamiast pętli po komórkach; wersja komórka po komórce na kilku tysiącach wierszy przekracza limit czasu wykonania.

Uruchom funkcję ręcznie raz. Google poprosi o autoryzację i pokaże ekran ostrzegawczy o niezweryfikowanej aplikacji; przy własnych skryptach to normalne, przechodzi się przez opcję zaawansowaną.

Wyzwalacze i harmonogram

Ręczne klikanie mija się z celem, więc ustaw wyzwalacz czasowy. W edytorze skryptu wybierz ikonę budzika, nowy wyzwalacz, funkcję pobierzDane, źródło czasowe i zegar dzienny na godzinę nocną. Najlepsze okno to 2–6 rano.

Google nie gwarantuje dokładnej minuty uruchomienia, tylko przedział godzinowy, a przy przeciążeniu potrafi pominąć jedno wykonanie. Dlatego okno siedmiodniowe jest celowe: nawet jeśli jedna noc wypadnie, kolejny przebieg pobierze brakujące dni. Ceną jest duplikacja wierszy, którą obsługujesz po stronie agregacji, licząc średnią po parze data plus zapytanie zamiast prostej sumy.

Dołóż też drugi wyzwalacz, który raz w tygodniu kopiuje zakładkę raw do osobnego pliku archiwum i czyści bieżącą. Arkusz Google ma twardy limit dziesięciu milionów komórek i przy kilku tysiącach wierszy dziennie osiągniesz go szybciej, niż się wydaje.

Alert mailowy przy spadku pozycji

Monitoring bez powiadomień to raport, do którego nikt nie zagląda. Druga funkcja porównuje średnią pozycję z ostatnich siedmiu dni ze średnią z poprzedniego tygodnia i wysyła maila tylko wtedy, gdy różnica przekroczy próg.

const PROG = 3;              // spadek o ile pozycji
const MIN_WYSWIETLEN = 50;   // ignoruj szum na malych frazach
const ODBIORCA = 'ty@twojadomena.pl';

function sprawdzSpadki() {
  const dane = SpreadsheetApp.getActive()
    .getSheetByName('agg').getDataRange().getValues();

  const alerty = dane.slice(1).filter(function (r) {
    const delta = r[3]-r[2];   // pozycja teraz minus pozycja wczesniej
    return delta >= PROG && r[4] >= MIN_WYSWIETLEN;
  });

  if (!alerty.length) return;

  const tresc = alerty.map(function (r) {
    return r[0] + ' : ' + r[2].toFixed(1) + ' na ' + r[3].toFixed(1);
  }).join('n');

  MailApp.sendEmail(ODBIORCA,
    'Spadki pozycji: ' + alerty.length + ' fraz',
    'Fraza, pozycja przed, pozycja po:nn' + tresc);
}

Próg minimalnych wyświetleń jest tu ważniejszy niż sam próg pozycji. Fraza z pięcioma wyświetleniami tygodniowo skacze o dziesięć pozycji bez żadnej przyczyny po Twojej stronie, bo średnia liczona jest z garstki zdarzeń. Bez filtra skrzynka zapełni się alarmami, których po tygodniu przestaniesz czytać. Wartość 50 sprawdza się dla małych i średnich witryn; przy dużym serwisie podnieś ją do kilkuset.

Drugi filtr warto dołożyć po pierwszym miesiącu: jeśli spadek dotyczy adresu, który właśnie przepisałeś albo scaliłeś, alert jest oczekiwany. Metodę oddzielania efektu własnej zmiany od tła opisuje materiał o tym, jak wygląda pomiar efektu aktualizacji artykułu.

Limity i typowe błędy

Darmowe konto Google daje na Apps Script około 90 minut czasu wykonania dziennie, sześć minut na pojedyncze uruchomienie i 100 maili na dobę z MailApp; konta Workspace mają te wartości wyższe. Search Analytics API dokłada własne kwoty i przy ich przekroczeniu zwraca błąd 429. Aktualne progi znajdziesz w tabeli limitów Apps Script.

Najczęstsze problemy w takim monitoringu i ich przyczyny:

  1. Pusty wynik mimo poprawnego kodu. Zwykle zły format identyfikatora witryny: sc-domain: dla weryfikacji domenowej kontra pełny URL dla prefiksu. Oba warianty potrafią istnieć w jednym koncie jako osobne usługi z różnymi danymi.
  2. Przekroczony czas wykonania. Zbyt szerokie okno dat połączone z zapisem komórka po komórce. Skracaj okno i zapisuj wsadowo.
  3. Liczby nie zgadzają się z interfejsem GSC. Efekt anonimizacji rzadkich zapytań oraz innej strefy czasowej. Search Console operuje na czasie pacyficznym Stanów Zjednoczonych, więc dzień w API bywa przesunięty względem doby w Polsce.
  4. Rosnące duplikaty. Nakładające się okna bez deduplikacji. Licz agregaty przez QUERY z grupowaniem, a nie przez sumę całej kolumny.
  5. Cichy brak danych po zmianie właściciela. Odebranie dostępu w Search Console nie wyłącza wyzwalacza; skrypt dalej działa i zapisuje zero wierszy. Dodaj log ostrzegawczy przy dwóch pustych przebiegach z rzędu.

Ostatnia uwaga dotyczy zakresu monitoringu. Arkusz pokazuje ruch z wyszukiwarki, ale coraz większa część odpowiedzi generowana jest przez modele językowe, które nie zostawiają śladu w Search Console. Aby zobaczyć pełny obraz, zestaw ten arkusz z sesjami z asystentów, opisanymi w poradniku o tym, jak wygląda ruch z ChatGPT i Perplexity w GA4, oraz z wyceną tego kanału, którą przybliża tekst o tym, jak liczyć wartość ruchu z AI i przypisany mu przychód. Dopiero te trzy źródła razem dają obraz, na podstawie którego można podejmować decyzje budżetowe.

Budowa całości zajmuje jedno popołudnie, a utrzymanie sprowadza się do przejrzenia alertów raz w tygodniu. W zamian masz archiwum, którego żaden dostawca nie zabierze Ci przy zmianie cennika.

FAQ

Czy monitoring w Google Sheets jest naprawdę darmowy

Tak, przy typowej skali. Arkusz, Apps Script i Search Console API mieszczą się w darmowych limitach konta Google. Koszt pojawia się dopiero przy bardzo dużych witrynach, gdzie dzienny wolumen wierszy wymusza archiwizację do BigQuery, a ta jest już płatna.

Dlaczego pozycja w arkuszu różni się od tego, co widzę w wyszukiwarce

Search Console podaje średnią ważoną pozycję ze wszystkich wyświetleń danego zapytania, z różnych urządzeń, lokalizacji i profili użytkowników. Twoje osobiste wyniki są personalizowane i pokazują jeden przypadek, a nie średnią. Rozbieżność rzędu kilku pozycji jest normalna.

Jak daleko wstecz sięgnę przy pierwszym uruchomieniu

API udostępnia te same 16 miesięcy co interfejs. Przy starcie warto jednorazowo pobrać cały ten okres w kilku przebiegach po kilkadziesiąt dni, żeby archiwum miało punkt odniesienia. Od tej pory skrypt tylko dopisuje nowe dni i historia rośnie bez limitu.

Czy skrypt zadziała dla kilku witryn naraz

Tak. Zamień stałą z adresem usługi na tablicę i przejdź po niej pętlą, dopisując identyfikator witryny jako dodatkową kolumnę w zakładce raw. Przy więcej niż kilku usługach rozważ osobny arkusz na każdą, bo wspólny plik szybko zbliża się do limitu komórek.

Co zrobić, gdy alert przychodzi codziennie

To sygnał, że próg jest za niski albo filtr wyświetleń zbyt luźny. Podnieś minimalną liczbę wyświetleń, wydłuż okno porównania z siedmiu do czternastu dni i ogranicz alerty do stron, które faktycznie generują konwersje. Powiadomienie ma prowokować decyzję, a nie opisywać szum.