Wyszukiwanie zmian w tagu GTM: porównywanie zapisanych i opublikowanych wersji

Management Summary
Google Tag Manager zachowuje wersje ustawień pomiaru witryny. Ich porównanie pomaga ustalić, kiedy zmienił się dany element, również po zmianie jego nazwy. Jest to przydatne, gdy po publikacji raport zaczyna pokazywać inne wartości, a przyczyna pozostaje niejasna. Zapisana wersja nie musi jednak oznaczać wersji faktycznie używanej, dlatego znaczenie ma okres publikacji. Znaleziona zmiana zawęża poszukiwania, ale sama nie dowodzi, że spowodowała przesunięcie wyników biznesowych.
Spis treści
Wartość wskaźnika zmienia się po kilku publikacjach GTM. Poszukiwanie przyczyny wymaga rozdzielenia dwóch pytań: W której zapisanej wersji zmieniła się konfiguracja tagu? Kiedy ta wersja rzeczywiście działała w analizowanej witrynie? Porównanie według tagId odpowiada na pierwsze pytanie. Historia publikacji i wybrane środowisko odpowiadają na drugie.
Procedura obejmuje pobranie pełnych wersji, prześledzenie tego samego tagu i szczegółowe porównanie istotnych przejść. Następnie sprawdzenia wymagają reguły, zmienne i rzeczywiste działanie.

1. Zapisana wersja a publikacja
Wersja GTM jest zapisanym stanem konfiguracji. Wersja może powstać bez publikacji, a starsza wersja może zostać opublikowana ponownie. Sortowanie według numeru wersji nie odtwarza więc pełnej osi czasu działania witryny. Przy analizie incydentu znaczenie mają czas publikacji, środowisko oraz kontener rzeczywiście osadzony na stronie. Interfejs GTM udostępnia historię publikacji.
Opisowa nazwa wersji ułatwia orientację. Nadal potrzebne są szczegóły i porównanie zmian. Od września 2026 GTM może proponować podsumowanie wygenerowane przez AI; takie podsumowanie nie zastępuje kontroli konfiguracji.
2. Pobranie pełnych wersji zamiast samych nagłówków
Metoda API version_headers.list zwraca między innymi identyfikatory wersji, nazwy i liczby elementów. Nie zawiera fingerprintów poszczególnych tagów. Porównanie tagów wymaga pełnych odpowiedzi versions.get. Pole nextPageToken oznacza kolejną stronę nagłówków. Pojedyncze żądanie listy nie zapewnia zatem kompletnego archiwum.
Wymagania to Python 3, włączone Tag Manager API i ważny token dostępu OAuth z zakresem https://www.googleapis.com/auth/tagmanager.readonly. Uwierzytelniona tożsamość musi dodatkowo mieć dostęp do danego kontenera GTM. Numeryczne identyfikatory API różnią się od publicznych oznaczeń takich jak GTM-XXXX. Konfiguracje i tokeny wymagają chronionego katalogu roboczego.
Poniższy przykład należy zapisać jako archive.py. Token jest przekazywany przez zmienną środowiskową GTM_ACCESS_TOKEN, bez umieszczania w kodzie lub publicznych logach. Katalog wynikowy musi być nowy. Kod pobiera wszystkie strony, sprawdza konto i kontener oraz zapisuje complete.json dopiero po zakończeniu wszystkich pobrań. Usunięte wersje są celowo wyłączone.
import json
import os
import re
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urlencode
from urllib.request import Request, urlopen
BASE = "https://tagmanager.googleapis.com/tagmanager/v2/"
def check_version(data, account, container, version):
expected = {"accountId": account, "containerId": container,
"containerVersionId": version}
if not isinstance(data, dict) or any(data.get(k) != v for k, v in expected.items()):
raise ValueError("Unexpected container/version identity")
if "error" in data or data.get("deleted", False) is not False:
raise ValueError("Error response or deleted version")
tags = data.get("tag", [])
if not isinstance(tags, list):
raise ValueError("Invalid tag list")
ids = []
for tag in tags:
if not isinstance(tag, dict) or not isinstance(tag.get("tagId"), str):
raise ValueError("Invalid tag")
ids.append(tag["tagId"])
if len(ids) != len(set(ids)):
raise ValueError("Duplicate tag IDs")
return data
def collect(account, container, output, get):
if not all(re.fullmatch(r"[0-9]+", x) for x in (account, container)):
raise ValueError("Numeric API account/container IDs required")
output = Path(output)
output.mkdir(mode=0o700) # Existing directory is an error.
parent = f"accounts/{account}/containers/{container}"
versions, seen_tokens, token = set(), set(), ""
while True:
query = {"includeDeleted": "false"}
if token:
query["pageToken"] = token
page = get(parent + "/version_headers?" + urlencode(query))
if not isinstance(page, dict) or "error" in page:
raise ValueError("Invalid headers response")
headers = page.get("containerVersionHeader", [])
if not isinstance(headers, list):
raise ValueError("Invalid headers list")
for item in headers:
if not isinstance(item, dict):
raise ValueError("Invalid header")
version = item.get("containerVersionId", "")
if (item.get("accountId") != account or item.get("containerId") != container
or not isinstance(version, str) or not re.fullmatch(r"[1-9][0-9]*", version)
or item.get("deleted", False) is not False or version in versions):
raise ValueError("Unexpected or duplicate version header")
versions.add(version)
token = page.get("nextPageToken", "")
if not isinstance(token, str) or token in seen_tokens:
raise ValueError("Invalid/repeated page token")
if not token:
break
seen_tokens.add(token)
for version in sorted(versions, key=int):
data = check_version(get(parent + "/versions/" + version), account, container, version)
temporary = output / ("version-" + version + ".tmp")
with temporary.open("x", encoding="utf-8") as handle:
json.dump(data, handle, ensure_ascii=False, indent=2)
temporary.replace(output / ("version-" + version + ".json"))
manifest = {"accountId": account, "containerId": container,
"versions": sorted(versions, key=int),
"completedAt": datetime.now(timezone.utc).isoformat()}
(output / "complete.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
return manifest
def main():
account, container, output = sys.argv[1:]
token = os.environ["GTM_ACCESS_TOKEN"]
os.umask(0o077)
def get(path):
time.sleep(5) # Other clients share the project's quota.
request = Request(BASE + path, headers={"Authorization": "Bearer " + token})
with urlopen(request, timeout=30) as response:
return json.load(response)
collect(account, container, output, get)
if __name__ == "__main__":
main()
python3 archive.py 6000000001 7000000002 archive-20261024
Błąd HTTP, wygasły token, nieprawidłowy JSON lub nieoczekiwany identyfikator przerywa działanie. Część plików może już istnieć, ale bez complete.json archiwum pozostaje niepełne. Po usunięciu przyczyny potrzebne jest nowe uruchomienie z nowym katalogiem. Nieznany obiekt błędu nie zostaje ponownie wykorzystany jako poprawna wersja kontenera.
Udokumentowane standardowe limity wynoszą 10 000 żądań na projekt dziennie oraz 25 żądań w ruchomym oknie 100 sekund. Przykład czeka pięć sekund przed każdym żądaniem. Pozostałe aplikacje w tym samym projekcie zużywają wspólny limit, więc przerwa nie gwarantuje powodzenia. Google dokumentuje HTTP 403 przy przekroczeniu limitu. Sposób rozwiązania zależy od konkretnego błędu; ogólna blokada do końca godziny nie jest udokumentowana.

3. Śledzenie zmian tego samego tagu
Zmiana nazwy istniejącego tagu nie zmienia identyfikatora tagId. Wybór odbywa się więc według tagId w obrębie tego samego konta i kontenera. Fingerprint API jest identyfikatorem zmiany obliczanym przez GTM. Dokumentacja opisuje ponowne obliczenie po modyfikacji, bez gwarancji kryptograficznej sumy kontrolnej lub dowodu zmiany działania pomiaru.
Plik tag_history.py znajduje się obok archive.py. Poniższe wywołanie analizuje przykładowy tag 25. Przykład obsługuje wyłącznie format archiwum poprzedniego skryptu, a nie inaczej opakowane eksporty JSON z interfejsu.
import difflib
import json
import sys
from pathlib import Path
from archive import check_version
def analyse(directory, tag_id):
directory = Path(directory)
manifest = json.loads((directory / "complete.json").read_text(encoding="utf-8"))
versions = manifest["versions"]
if not versions or len(set(versions)) != len(versions):
raise ValueError("Empty or duplicate version manifest")
previous, first, rows, diffs = None, True, [], []
for version in sorted(versions, key=int):
if not isinstance(version, str) or not version.isascii() or not version.isdecimal():
raise ValueError("Invalid version ID")
data = json.loads((directory / f"version-{version}.json").read_text(encoding="utf-8"))
check_version(data, manifest["accountId"], manifest["containerId"], version)
tag = next((t for t in data.get("tag", []) if t["tagId"] == tag_id), None)
if tag is not None and (not isinstance(tag.get("fingerprint"), str) or not tag["fingerprint"]):
raise ValueError("Missing tag fingerprint")
state = ("BASELINE_PRESENT" if tag is not None else "BASELINE_ABSENT") if first else (
"ADDED" if previous is None and tag is not None else
"REMOVED" if previous is not None and tag is None else
"ABSENT" if tag is None else
"CHANGED" if previous["fingerprint"] != tag["fingerprint"] else "UNCHANGED")
rows.append((version, state))
if not first and state in {"ADDED", "REMOVED", "CHANGED"}:
def normalise(value):
if value is None:
return "null\n"
metadata = {"path", "accountId", "containerId", "workspaceId", "fingerprint", "tagManagerUrl"}
return json.dumps({k: v for k, v in value.items() if k not in metadata},
ensure_ascii=False, sort_keys=True, indent=2) + "\n"
diffs.append("".join(difflib.unified_diff(
normalise(previous).splitlines(keepends=True),
normalise(tag).splitlines(keepends=True),
fromfile="previous saved version", tofile="version " + version)))
previous, first = tag, False
return rows, diffs
if __name__ == "__main__":
rows, diffs = analyse(sys.argv[1], sys.argv[2])
for version, state in rows:
print(version, state)
for diff in diffs:
print(diff)
python3 tag_history.py archive-20261024 25
BASELINE_PRESENT i BASELINE_ABSENT opisują tylko pierwszy analizowany stan. ADDED oznacza przejście od braku do obecności, a REMOVED przejście odwrotne. CHANGED wskazuje inny fingerprint, natomiast UNCHANGED taki sam. ABSENT oznacza dalszy brak tagu. Ponowne pojawienie się z identycznym fingerprintem pozostaje zatem widoczne.
Wynik „wersja 44: CHANGED” zawęża zmianę do przejścia między dwoma zapisanymi stanami. Brakujące wersje, niezapisane modyfikacje pośrednie lub niedostępny początek historii ograniczają wniosek. Pierwsza dostępna wersja nie dowodzi automatycznie pierwotnego czasu utworzenia tagu.
4. Interpretacja różnicy
Dla istotnych przejść skrypt tworzy porównanie tekstowe pokazujące dodane i usunięte wiersze. Pomijane są wyłącznie jawnie wymienione pola administracyjne na najwyższym poziomie obiektu tagu. Nazwy, notatki i parametry pozostają. Sortowanie porządkuje klucze obiektów, ale zachowuje kolejność tablic. Zmiana nazwy nadal pojawia się jako różnica konfiguracji. Puste porównanie mimo nowego fingerprintu może wynikać z pominiętych metadanych.
Ważne punkty kontroli obejmują wartości parametrów, reguły uruchamiające i blokujące, wstrzymanie tagu, ustawienia zgody oraz tagi uruchamiane przed głównym tagiem i po głównym tagu. Niezmieniony tag może działać inaczej po zmianie powiązanej zmiennej, reguły, szablonu lub zewnętrznej wartości danych. Analiza nie powinna więc kończyć się na fingerprincie głównego tagu. Porównanie całych kontenerów pomaga znaleźć dalsze zależności, lecz nie stanowi automatycznej analizy wpływu na działanie.
Przy większych porównaniach procedurę uzupełnia normalizacja dwóch eksportów GTM przed porównaniem. Szczególnej uwagi wymagają różne formaty eksportu i kolejność tablic mająca znaczenie dla działania.

5. Zmiana konfiguracji a przyczyna problemu
Parametr zmieniony w odpowiednim czasie jest wskazówką do dalszej analizy. Potwierdzenie związku przyczynowego ze wskaźnikiem wymaga dodatkowo kontrolowanego testu funkcjonalnego, na przykład w środowisku testowym z Tag Assistant, oraz odpowiednich danych sieciowych i zdarzeń. Osoba publikująca wersję i autorstwo każdej pojedynczej modyfikacji również stanowią odrębne informacje.
Od aktualizacji GTM z 8 października 2026 widoczne zdarzenia gtag.config mogą trafiać do reguł z wildcard; inicjalizacja następuje przy załadowaniu kontenera. Takich zmian oprogramowania wykonawczego Google nie wyjaśnia samo porównanie zapisanych tagów. Osobno oferowana optymalizacja kontenera może natomiast wprowadzać zmiany do obszaru roboczego. Według Google uprawnienia nie są wersjonowane: przywrócenie wersji nie odtwarza wcześniejszych praw dostępu. Omówienie zmian GTM i tagu Google w 2026 roku rozwija to rozróżnienie.
Pytania i odpowiedzi
Czy udane pobranie oznacza pełne archiwum historyczne?
Archiwum obejmuje nieusunięte wersje dostępne dla uwierzytelnionej tożsamości podczas pobierania. Żądania są wykonywane kolejno. Archiwum nie przedstawia więc wszystkich wersji z dokładnie tej samej chwili. Równoczesne zmiany lub usunięcia mogą spowodować przerwanie procesu albo zbiór pochodzący z różnych chwil. Spokojne okno konserwacyjne i dodatkowe porównanie z listą wersji poprawiają możliwość weryfikacji. Czas zakończenia zapisany w manifeście nie jest datą publikacji.
Czy istniejący plik pozostaje zawsze aktualny?
Plik dokumentuje odpowiedź z określonego momentu pobrania. Metadane wersji mogą się zmieniać; API udostępnia operację aktualizacji wersji. Archiwum powinno więc zachowywać pochodzenie i czas pobrania. Sama nazwa pliku nie dowodzi poprawności odpowiedzi ani kompletności i aktualności zbioru.
Dlaczego brakujące pliki i fingerprinty przerywają analizę?
Ciche pomijanie brakujących danych mogłoby przedstawić niepełne archiwum jako pozornie niezmienioną historię. Przerwanie odróżnia błąd techniczny od rzeczywistego braku szukanego tagu. Walidacja obejmuje tożsamość i strukturę potrzebną do porównania, a nie wszystkie możliwe pola GTM.
Czy taki sam fingerprint dowodzi identycznego działania?
Nie. Porównanie dotyczy zapisanego tagu. Zależności, kod witryny, stan zgody, dane i później ładowane oprogramowanie wykonawcze mogą wpływać na zachowanie. Fingerprint nie zastępuje analizy tych czynników ani testu funkcjonalnego.
Źródła i dalsza dokumentacja
- Publikowanie, wersje i zatwierdzanie
- version_headers.list
- ContainerVersionHeader
- versions.get
- ContainerVersion
- Tag
- Limity API
- Uprawnienia dostępu do API
- versions.update
- Informacje o wersjach GTM
- Optymalizacja kontenera
- Ujednolicona konfiguracja tagu Google
- Obsługiwana instalacja tagu Google
- Inicjalizacja GTM
- GTM Web


