{"id":8812,"date":"2026-08-05T15:36:34","date_gmt":"2026-08-05T13:36:34","guid":{"rendered":"https:\/\/www.lukaswojcik.com\/blog\/?p=8812"},"modified":"2026-10-23T15:00:33","modified_gmt":"2026-10-23T13:00:33","slug":"gtm-container-history-deep-search-engine","status":"publish","type":"page","link":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/gtm-container-history-deep-search-engine\/","title":{"rendered":"GTM Container History &amp; Deep Search Engine"},"content":{"rendered":"<div class=\"gtm-analyser-container lw-tool-surface\" style=\"background: var(--bg-panel, #1e1e24); padding: 25px; border-radius: 8px; border: 1px solid var(--border, #2a2a35);\">\n<div class=\"lw-aussenhinweis\" style=\"background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); border-left: 3px solid var(--lw-tool-warn); border-radius: 6px; padding: 14px 16px; margin-bottom: 20px;\">\n<div style=\"color: var(--lw-tool-warn); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 6px;\">Needs a Google sign-in<\/div>\n<p style=\"color: var(--text-secondary, #a0a0b0); margin: 0 0 8px; font-size: 0.9rem; line-height: 1.55;\">Unlike the other tools here, this one does not work on its own. It reads from the Google Tag Manager REST API v2 and therefore asks for a Google sign-in. The permission requested is tagmanager.readonly \u2014 read-only, so nothing can be changed through it.<\/p>\n<p style=\"color: var(--text-secondary, #a0a0b0); margin: 0; font-size: 0.9rem; line-height: 1.55;\">What it fetches stays in this tab: the evaluation runs in the browser, and nothing is sent to lukaswojcik.com. The OAuth Client ID below is prefilled and can be replaced with a different one.<\/p>\n<\/div>\n<p style=\"color: var(--text-secondary); margin-bottom: 20px; line-height: 1.6;\">\n        Connect directly to the <strong>Google Tag Manager REST API v2<\/strong> using OAuth 2.0.<br \/>\n        Fetch version headers and deeply search through hundreds of container versions for tags, scripts, triggers, or variables without manual file exports.\n    <\/p>\n<p id=\"gtm-element-history-intro\" style=\"color: var(--text-secondary); margin-bottom: 20px; line-height: 1.6;\"><strong>Change history of one element:<\/strong> after signing in, a single tag, trigger, variable, template, folder, client, transformation or zone can be entered by name, ID or GTM URL. The tool lists every version in which it was created, changed, renamed, removed or restored, with version number, version name and notes, dates, field-by-field differences, changes to referenced triggers and variables, and unpublished changes in workspaces.<\/p>\n<p id=\"gtm-unification-background\" style=\"color: var(--text-secondary); margin-bottom: 20px; line-height: 1.6;\"><strong>Earlier overview of the unification:<\/strong> <a href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/google-tag-and-tag-manager-merge-destinations-the-gtm-init-trigger-and-what-the-loading-id-decides\/\">Google Tag and Tag Manager Merge: Destinations, the gtm init Trigger and What the Loading ID Decides<\/a>.<\/p>\n<p id=\"gtm-unification-article\" style=\"color: var(--text-secondary); margin-bottom: 20px; line-height: 1.6;\"><strong>Background to the migration:<\/strong> <a href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/google-tag-manager-and-gtag-js-merge-what-changes-in-2026\/\">Google Tag Manager and gtag.js Merge: What Changes in 2026<\/a>. The October overview explains container optimization, gtag.config events and a controlled migration.<\/p>\n<div id=\"gtm-tool-update-20261019\" style=\"border-left:3px solid var(--accent);padding:12px 16px;margin:0 0 24px;background:var(--bg-body);border-radius:6px;color:var(--text-secondary);line-height:1.6;\">\n<p style=\"margin:0 0 8px;\"><strong>Update \u2014 19.10.2026:<\/strong> The search covers the selected tag, trigger, variable, template and folder lists in the returned versions. It is not a search of every container-wide setting or permission. Google-delivered runtime can change without a new version; no result for <code>gtag.config<\/code> does not rule out that runtime event. The optional optimization requires a separate review of central settings and destination tags. Permissions are not versioned and are not restored by a version rollback.<\/p>\n<p style=\"margin:0;font-size:0.9em;\"><a href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/google-tag-manager-and-gtag-js-merge-what-changes-in-2026\/\">October changes in context<\/a> \u00b7 <a href=\"https:\/\/support.google.com\/tagmanager\/answer\/4620708?hl=en\">Google release notes<\/a> \u00b7 <a href=\"https:\/\/support.google.com\/analytics\/answer\/16659369\">Optimization and permissions<\/a><\/p>\n<\/div>\n<div id=\"gtm-related-links-20261020\" class=\"lw-gtm-related\" style=\"margin:20px 0;color:var(--text-secondary);line-height:1.6;\">\n<p><strong>Related guides and tools:<\/strong><\/p>\n<ul>\n<li><a href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/tutorials\/tutorial-finding-the-container-version-in-which-a-tag-changed\/\">Finding the version where a tag changed<\/a> \u2013 version archives, fingerprints and the limits of historical evidence.<\/li>\n<li><a href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/web-development\/comparing-two-gtm-container-exports-normalising-before-the-diff-and-what-a-line-diff-cannot-show\/\">Normalizing and comparing GTM exports<\/a> \u2013 configuration differences and the limits of a text comparison.<\/li>\n<\/ul>\n<\/div>\n<p>    <!-- STEP 1: OAUTH & API AUTHENTICATION --><\/p>\n<div style=\"background: var(--bg-body, #101416); padding: 20px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 20px;\">\n<h3 style=\"color: var(--accent); font-size: 1rem; margin-top: 0; margin-bottom: 12px;\">1. Google API Authentication<\/h3>\n<div style=\"display: flex; gap: 15px; flex-wrap: wrap; align-items: flex-end;\">\n<div style=\"flex: 1; min-width: 280px;\">\n                <label style=\"color: var(--text-secondary); display: block; font-size: 0.85rem; margin-bottom: 6px;\" for=\"api-client-id\">Google Cloud OAuth Client ID:<\/label><br \/>\n                <input type=\"text\" id=\"api-client-id\" value=\"423029313598-2ko0jilc28eevp253rqncvodg2181e1l.apps.googleusercontent.com\" class=\"form-control\" style=\"width: 100%; padding: 10px; background: var(--bg-panel); border: 1px solid var(--border); color: var(--text-primary); border-radius: 6px; font-family: monospace;\">\n            <\/div>\n<div>\n                <button id=\"auth-btn\" class=\"button\" style=\"background: var(--accent); color:var(--on-accent); border: none; padding: 11px 22px; border-radius: 6px; font-weight: 700; cursor: pointer;\">Authorize with Google<\/button>\n            <\/div>\n<\/p><\/div>\n<div data-mp-mask id=\"auth-status\" style=\"margin-top: 10px; font-size: 0.85rem; color: var(--text-secondary);\">Status: <strong style=\"color: var(--fail);\">Not connected<\/strong><\/div>\n<\/p><\/div>\n<p>    <!-- STEP 2: CONTAINER SELECTOR --><\/p>\n<div id=\"container-selector-panel\" style=\"background: var(--bg-body); padding: 20px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 20px; display: none;\">\n<h3 style=\"color: var(--accent); font-size: 1rem; margin-top: 0; margin-bottom: 12px;\">2. Select GTM Container &amp; Version Range<\/h3>\n<div style=\"display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 15px;\">\n<div>\n                <label style=\"color: var(--text-secondary); display: block; font-size: 0.85rem; margin-bottom: 6px;\">GTM Account:<\/label><br \/>\n                <select data-mp-mask id=\"account-select\" style=\"width: 100%; padding: 10px; background: var(--bg-panel); color: var(--text-primary); border: 1px solid var(--border); border-radius: 6px;\"><option value=\"\">&#8212; Select Account &#8212;<\/option><\/select>\n            <\/div>\n<div>\n                <label style=\"color: var(--text-secondary); display: block; font-size: 0.85rem; margin-bottom: 6px;\">Container:<\/label><br \/>\n                <select data-mp-mask id=\"container-select\" style=\"width: 100%; padding: 10px; background: var(--bg-panel); color: var(--text-primary); border: 1px solid var(--border); border-radius: 6px;\" disabled><option value=\"\">&#8212; Select Container &#8212;<\/option><\/select>\n            <\/div>\n<div>\n                <label style=\"color: var(--text-secondary); display: block; font-size: 0.85rem; margin-bottom: 6px;\">Versions Range to Scan:<\/label><br \/>\n                <select id=\"range-select\" style=\"width: 100%; padding: 10px; background: var(--bg-panel); color: var(--text-primary); border: 1px solid var(--border); border-radius: 6px;\" disabled><option value=\"30\">Latest 30 Versions (Fast)<\/option><option value=\"100\">Latest 100 Versions<\/option><option value=\"300\">Latest 300 Versions<\/option><option value=\"all\">ALL Versions (Enterprise Scan)<\/option><\/select>\n            <\/div>\n<\/p><\/div>\n<div data-mp-mask id=\"header-status\" style=\"font-size: 0.85rem; color: var(--text-secondary);\"><\/div>\n<\/p><\/div>\n<p>    <!-- MODE TABS: DEEP SEARCH \/ ELEMENT HISTORY (23.10.2026) --><\/p>\n<div id=\"gtm-mode-tabs\" role=\"tablist\" style=\"display: none; gap: 8px; flex-wrap: wrap; margin-bottom: 15px;\">\n        <button type=\"button\" role=\"tab\" class=\"gtm-mode-tab button\" data-mode=\"search\" aria-selected=\"true\" style=\"border: 1px solid var(--accent); padding: 10px 18px; border-radius: 6px; font-weight: 700; cursor: pointer;\">Deep search<\/button><br \/>\n        <button type=\"button\" role=\"tab\" class=\"gtm-mode-tab button\" data-mode=\"history\" aria-selected=\"false\" style=\"border: 1px solid var(--accent); padding: 10px 18px; border-radius: 6px; font-weight: 700; cursor: pointer;\">Change history of one element<\/button>\n    <\/div>\n<p>    <!-- STEP 3: DEEP SEARCH CONTROLS --><\/p>\n<div id=\"search-controls-panel\" style=\"background: var(--bg-body); padding: 20px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 25px; display: none;\">\n<h3 style=\"color: var(--accent); font-size: 1rem; margin-top: 0; margin-bottom: 12px;\">3. Deep Historical Search<\/h3>\n<div style=\"display: flex; gap: 15px; flex-wrap: wrap; margin-bottom: 15px;\">\n<div style=\"flex: 1; min-width: 260px;\">\n                <input type=\"text\" id=\"search-query\" placeholder=\"e.g., G-XXXXXXX, fbq(, analytics_storage, or ^gtm\\..*\" style=\"width: 100%; padding: 12px; background: var(--bg-panel); border: 1px solid var(--border); color: var(--text-primary); border-radius: 6px; font-family: monospace;\">\n            <\/div>\n<div>\n                <button id=\"run-api-search-btn\" class=\"button\" style=\"background: var(--accent); color:var(--on-accent); border: none; padding: 12px 28px; border-radius: 6px; font-weight: 700; cursor: pointer;\">Run Deep API Search<\/button>\n            <\/div>\n<\/p><\/div>\n<div style=\"display: flex; gap: 15px; flex-wrap: wrap; align-items: center; border-top: 1px solid var(--border); padding-top: 15px; font-size: 0.85rem;\">\n            <label style=\"color: var(--text-secondary); cursor: pointer;\"><input type=\"checkbox\" id=\"opt-regex\"> Use Regex<\/label><br \/>\n            <label style=\"color: var(--text-secondary); cursor: pointer;\"><input type=\"checkbox\" id=\"opt-case\"> Case Sensitive<\/label><br \/>\n            <span style=\"color: var(--border);\">|<\/span><br \/>\n            <span style=\"color: var(--text-primary); font-weight: bold;\">Scan Scopes:<\/span><br \/>\n            <label style=\"color: var(--text-secondary); cursor: pointer;\"><input type=\"checkbox\" class=\"scope-cb\" value=\"tag\" checked> Tags<\/label><br \/>\n            <label style=\"color: var(--text-secondary); cursor: pointer;\"><input type=\"checkbox\" class=\"scope-cb\" value=\"trigger\" checked> Triggers<\/label><br \/>\n            <label style=\"color: var(--text-secondary); cursor: pointer;\"><input type=\"checkbox\" class=\"scope-cb\" value=\"variable\" checked> Variables<\/label><br \/>\n            <label style=\"color: var(--text-secondary); cursor: pointer;\"><input type=\"checkbox\" class=\"scope-cb\" value=\"customTemplate\" checked> Templates<\/label><br \/>\n            <label style=\"color: var(--text-secondary); cursor: pointer;\"><input type=\"checkbox\" class=\"scope-cb\" value=\"folder\" checked> Folders<\/label>\n        <\/div>\n<p>        <!-- PROGRESS BAR --><\/p>\n<div id=\"progress-container\" style=\"margin-top: 15px; display: none;\">\n<div style=\"display: flex; justify-content: space-between; font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 5px;\">\n                <span data-mp-mask id=\"progress-text\">Scanning GTM API versions&#8230;<\/span><br \/>\n                <span data-mp-mask id=\"progress-percent\">0%<\/span>\n            <\/div>\n<div style=\"width: 100%; height: 8px; background: var(--bg-raised); border-radius: 4px; overflow: hidden;\">\n<div id=\"progress-bar\" style=\"width: 0%; height: 100%; background: var(--ok); transition: width 0.2s;\"><\/div>\n<\/p><\/div>\n<\/p><\/div>\n<\/p><\/div>\n<p>    <!-- STEP 3 (ALTERNATIVE): CHANGE HISTORY OF ONE ELEMENT (23.10.2026) --><\/p>\n<div id=\"element-history-panel\" style=\"background: var(--bg-body); padding: 20px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 25px; display: none;\">\n<h3 style=\"color: var(--accent); font-size: 1rem; margin-top: 0; margin-bottom: 12px;\">3. Change History of One Element<\/h3>\n<p style=\"color: var(--text-secondary); font-size: 0.9rem; line-height: 1.55; margin: 0 0 15px;\">Enter a name, an ID or a URL copied from Tag Manager (\u2026\/tags\/12). The element is followed by its ID, so renames stay in one history. The version range from step 2 applies; versions already read are kept for the next element.<\/p>\n<div style=\"display: flex; gap: 15px; flex-wrap: wrap; align-items: flex-end; margin-bottom: 15px;\">\n<div style=\"min-width: 170px;\">\n                <label style=\"color: var(--text-secondary); display: block; font-size: 0.85rem; margin-bottom: 6px;\" for=\"eh-type\">Element type:<\/label><br \/>\n                <select id=\"eh-type\" style=\"width: 100%; padding: 11px 10px; background: var(--bg-panel); color: var(--text-primary); border: 1px solid var(--border); border-radius: 6px;\"><option value=\"any\">Any type<\/option><option value=\"tag\">Tag<\/option><option value=\"trigger\">Trigger<\/option><option value=\"variable\">Variable<\/option><option value=\"builtInVariable\">Built-in variable<\/option><option value=\"customTemplate\">Template<\/option><option value=\"folder\">Folder<\/option><option value=\"client\">Client (server)<\/option><option value=\"transformation\">Transformation (server)<\/option><option value=\"zone\">Zone<\/option><\/select>\n            <\/div>\n<div style=\"flex: 1; min-width: 240px;\">\n                <label style=\"color: var(--text-secondary); display: block; font-size: 0.85rem; margin-bottom: 6px;\" for=\"eh-query\">Name, ID or GTM URL:<\/label><br \/>\n                <input type=\"text\" id=\"eh-query\" list=\"eh-suggestions\" autocomplete=\"off\" data-mp-mask data-rc-private placeholder=\"e.g. GA4 - purchase, 42 or https:\/\/tagmanager.google.com\/#\/container\/\u2026\/tags\/42\" style=\"width: 100%; padding: 11px 12px; background: var(--bg-panel); border: 1px solid var(--border); color: var(--text-primary); border-radius: 6px; font-family: monospace;\"><br \/>\n                <datalist id=\"eh-suggestions\"><\/datalist>\n            <\/div>\n<div>\n                <button id=\"eh-run-btn\" type=\"button\" class=\"button\" style=\"background: var(--accent); color: var(--on-accent); border: none; padding: 12px 28px; border-radius: 6px; font-weight: 700; cursor: pointer;\">Show change history<\/button>\n            <\/div>\n<\/p><\/div>\n<div style=\"display: flex; gap: 15px; flex-wrap: wrap; align-items: center; border-top: 1px solid var(--border); padding-top: 15px; font-size: 0.85rem;\">\n            <label style=\"color: var(--text-secondary); cursor: pointer;\"><input type=\"checkbox\" id=\"eh-deps\" checked> Include referenced triggers and variables<\/label><br \/>\n            <label style=\"color: var(--text-secondary); cursor: pointer;\"><input type=\"checkbox\" id=\"eh-drafts\" checked> Include unpublished workspace changes<\/label><br \/>\n            <label style=\"color: var(--text-secondary); cursor: pointer;\"><input type=\"checkbox\" id=\"eh-touched\"> Show saves without a configuration difference<\/label>\n        <\/div>\n<div data-mp-mask id=\"eh-status\" style=\"margin-top: 12px; font-size: 0.85rem; color: var(--text-secondary);\"><\/div>\n<div id=\"eh-progress\" style=\"margin-top: 15px; display: none;\">\n<div style=\"display: flex; justify-content: space-between; font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 5px;\">\n                <span data-mp-mask id=\"eh-progress-text\"><\/span><br \/>\n                <span data-mp-mask id=\"eh-progress-percent\">0%<\/span>\n            <\/div>\n<div style=\"width: 100%; height: 8px; background: var(--bg-raised); border-radius: 4px; overflow: hidden;\">\n<div id=\"eh-progress-bar\" style=\"width: 0%; height: 100%; background: var(--ok); transition: width 0.2s;\"><\/div>\n<\/p><\/div>\n<\/p><\/div>\n<\/p><\/div>\n<p>    <!-- RESULTS --><\/p>\n<div data-mp-mask id=\"api-search-results\"><\/div>\n<div data-mp-mask data-rc-private id=\"element-history-results\" style=\"display: none;\"><\/div>\n<\/div>\n<p><script>\n(() => {\n    let accessToken = null;\n    let tokenClient = null;\n    let versionHeaders = [];\n    const accountsMap = {};\n    const containersMap = {};<\/p>\n<p>    const authBtn = document.getElementById(\"auth-btn\");\n    const authStatus = document.getElementById(\"auth-status\");\n    const accSelect = document.getElementById(\"account-select\");\n    const contSelect = document.getElementById(\"container-select\");\n    const rangeSelect = document.getElementById(\"range-select\");\n    const headerStatus = document.getElementById(\"header-status\");\n    const resultsEl = document.getElementById(\"api-search-results\");\n    const progressCont = document.getElementById(\"progress-container\");\n    const progressBar = document.getElementById(\"progress-bar\");\n    const progressText = document.getElementById(\"progress-text\");\n    const progressPercent = document.getElementById(\"progress-percent\");<\/p>\n<p>    const esc = (str) => String(str).replace(\/&\/g, \"&amp;\").replace(\/<\/g, \"&lt;\").replace(\/>\/g, \"&gt;\");<\/p>\n<p>    \/\/ 1. OAUTH 2.0 INITIALIZATION\n    authBtn.addEventListener(\"click\", () => {\n        const clientId = document.getElementById(\"api-client-id\").value.trim();\n        if (!clientId) {\n            alert(\"Please enter a valid Google Cloud Client ID.\");\n            return;\n        }<\/p>\n<p>        if (!window.google || !google.accounts || !google.accounts.oauth2) {\n            \/\/ Google Identity Services nur nach Einwilligung (consented.eu), siehe js\/lw-google-login.js\n            if (window.lwGoogleSdk) { window.lwGoogleSdk().then(() => authBtn.click(), () => {}); }\n            return;\n        }<\/p>\n<p>        tokenClient = google.accounts.oauth2.initTokenClient({\n            client_id: clientId,\n            scope: \"https:\/\/www.googleapis.com\/auth\/tagmanager.readonly\",\n            callback: (resp) => {\n                if (resp.error) {\n                    authStatus.innerHTML = \"Status: <strong style='color:#ff6b6b;'>Auth Error: \" + esc(resp.error) + \"<\/strong>\";\n                    return;\n                }\n                accessToken = resp.access_token;\n                authStatus.innerHTML = \"Status: <strong style='color:#51cf66;'>Connected (Read-Only Access)<\/strong>\";\n                document.getElementById(\"container-selector-panel\").style.display = \"block\";\n                loadAccounts();\n            }\n        });\n        tokenClient.requestAccessToken();\n    });<\/p>\n<p>    \/\/ 2. API FETCHER WITH RATE-LIMIT THROTTLING & ADVANCED ERROR DECODING\n    const gtmApi = async (path) => {\n        const res = await fetch(\"https:\/\/tagmanager.googleapis.com\/tagmanager\/v2\/\" + path, {\n            headers: { \"Authorization\": \"Bearer \" + accessToken }\n        });\n        if (res.status === 429) {\n            await new Promise(r => setTimeout(r, 1500));\n            return gtmApi(path);\n        }\n        if (!res.ok) {\n            let errDetails = res.statusText || \"Unknown API Error\";\n            try {\n                const errJson = await res.json();\n                if (errJson && errJson.error && errJson.error.message) {\n                    errDetails = errJson.error.message;\n                }\n            } catch (e) {}\n            throw new Error(\"API Error \" + res.status + \": \" + errDetails);\n        }\n        return res.json();\n    };<\/p>\n<p>    const loadAccounts = async () => {\n        try {\n            accSelect.innerHTML = \"<option>Loading accounts...<\/option>\";\n            const data = await gtmApi(\"accounts\");\n            accSelect.innerHTML = \"<option value=''>-- Select Account --<\/option>\";\n            (data.account || []).forEach(a => {\n                accountsMap[a.accountId] = a.name;\n                const opt = document.createElement(\"option\");\n                opt.value = a.accountId;\n                opt.textContent = a.name + \" (\" + a.accountId + \")\";\n                accSelect.appendChild(opt);\n            });\n        } catch (e) {\n            accSelect.innerHTML = \"<option>Error loading accounts<\/option>\";\n            alert(\"Failed to fetch accounts: \" + e.message);\n        }\n    };<\/p>\n<p>    accSelect.addEventListener(\"change\", async () => {\n        const accId = accSelect.value;\n        if (!accId) return;\n        try {\n            contSelect.innerHTML = \"<option>Loading containers...<\/option>\";\n            contSelect.disabled = true;\n            const data = await gtmApi(\"accounts\/\" + accId + \"\/containers\");\n            contSelect.innerHTML = \"<option value=''>-- Select Container --<\/option>\";\n            (data.container || []).forEach(c => {\n                containersMap[c.containerId] = c;\n                const opt = document.createElement(\"option\");\n                opt.value = c.containerId;\n                opt.textContent = c.name + \" (\" + c.publicId + \")\";\n                contSelect.appendChild(opt);\n            });\n            contSelect.disabled = false;\n        } catch (e) {\n            alert(\"Failed to load containers: \" + e.message);\n        }\n    });<\/p>\n<p>    contSelect.addEventListener(\"change\", async () => {\n        const accId = accSelect.value;\n        const contId = contSelect.value;\n        if (!contId) return;\n        try {\n            headerStatus.innerHTML = \"Fetching metadata for all container versions...\";\n            const data = await gtmApi(\"accounts\/\" + accId + \"\/containers\/\" + contId + \"\/version_headers\");\n            versionHeaders = (data.containerVersionHeader || []).sort((a, b) => Number(b.containerVersionId) - Number(a.containerVersionId));\n            headerStatus.innerHTML = \"<strong style='color:#51cf66;'>Success:<\/strong> Found <strong>\" + versionHeaders.length + \" versions<\/strong> in this container.\";\n            rangeSelect.disabled = false;\n            ehContainerReady();\n        } catch (e) {\n            headerStatus.innerHTML = \"<span style='color:#ff6b6b;'>Error loading version headers: \" + esc(e.message) + \"<\/span>\";\n        }\n    });<\/p>\n<p>    \/\/ 3. DEEP SEARCH TRAVERSAL LOGIC\n    const searchInObject = (obj, regex, path = \"\") => {\n        const matches = [];\n        if (typeof obj === \"string\") {\n            if (regex.test(obj)) matches.push({ path: path || \"value\", snippet: obj });\n        } else if (Array.isArray(obj)) {\n            obj.forEach((item, idx) => matches.push(...searchInObject(item, regex, path ? path + \"[\" + idx + \"]\" : \"[\" + idx + \"]\")));\n        } else if (obj && typeof obj === \"object\") {\n            for (const key in obj) {\n                if (Object.prototype.hasOwnProperty.call(obj, key)) {\n                    const val = obj[key];\n                    const nextPath = path ? path + \".\" + key : key;\n                    if (regex.test(key)) matches.push({ path: nextPath, snippet: typeof val === \"object\" ? JSON.stringify(val) : String(val) });\n                    matches.push(...searchInObject(val, regex, nextPath));\n                }\n            }\n        }\n        return matches;\n    };<\/p>\n<p>    const getBadge = (type) => {\n        const colors = {\n            tag: { bg: \"#1a3a4b\", color: \"#51cf66\", label: \"TAG\" },\n            trigger: { bg: \"#4b3a1a\", color: \"#ff922b\", label: \"TRIGGER\" },\n            variable: { bg: \"#2a1a4b\", color: \"#339af0\", label: \"VARIABLE\" },\n            customTemplate: { bg: \"#4b1a47\", color: \"#cc5de8\", label: \"TEMPLATE\" },\n            folder: { bg: \"#242b30\", color: \"#a0a6b0\", label: \"FOLDER\" }\n        };\n        const c = colors[type] || { bg: \"#242b30\", color: \"#ffffff\", label: type.toUpperCase() };\n        return \"<span style='background:\" + c.bg + \"; color:\" + c.color + \"; padding:3px 8px; border-radius:4px; font-size:0.75rem; font-weight:700;'>\" + c.label + \"<\/span>\";\n    };<\/p>\n<p>    \/\/ 4. QUEUED SCANNER ENGINE\n    document.getElementById(\"run-api-search-btn\").addEventListener(\"click\", async () => {\n        const query = document.getElementById(\"search-query\").value.trim();\n        if (!query) { alert(\"Please enter a search query.\"); return; }<\/p>\n<p>        const isRegex = document.getElementById(\"opt-regex\").checked;\n        const isCase = document.getElementById(\"opt-case\").checked;\n        const activeScopes = Array.from(document.querySelectorAll(\".scope-cb:checked\")).map(cb => cb.value);<\/p>\n<p>        let regex;\n        try {\n            const flags = isCase ? \"g\" : \"gi\";\n            regex = isRegex ? new RegExp(query, flags) : new RegExp(query.replace(\/[.*+?^${}()|[\\]\\\\]\/g, \"\\\\$&\"), flags);\n        } catch (e) {\n            alert(\"Invalid Regex: \" + e.message); return;\n        }<\/p>\n<p>        const rangeVal = rangeSelect.value;\n        const toScan = rangeVal === \"all\" ? versionHeaders : versionHeaders.slice(0, Number(rangeVal));<\/p>\n<p>        resultsEl.innerHTML = \"\";\n        progressCont.style.display = \"block\";\n        const allResults = [];\n        let completed = 0;<\/p>\n<p>        \/\/ Run batch pool of 3 concurrent API calls to avoid 429 limit\n        const concurrency = 3;\n        for (let i = 0; i < toScan.length; i += concurrency) {\n            const batch = toScan.slice(i, i + concurrency);\n            await Promise.all(batch.map(async (vh) => {\n                try {\n                    const fullVer = await gtmApi(vh.path);\n                    activeScopes.forEach((scope) => {\n                        const items = fullVer[scope] || [];\n                        items.forEach((item) => {\n                            const itemMatches = searchInObject(item, regex);\n                            if (itemMatches.length > 0) {\n                                allResults.push({\n                                    versionId: vh.containerVersionId,\n                                    versionName: vh.name || \"(Unnamed)\",\n                                    type: scope,\n                                    entityName: item.name || \"(Unnamed Item)\",\n                                    matches: itemMatches\n                                });\n                            }\n                        });\n                    });\n                } catch (err) {\n                    console.error(\"Error scanning version \" + vh.containerVersionId, err);\n                } finally {\n                    completed++;\n                    const pct = Math.round((completed \/ toScan.length) * 100);\n                    progressBar.style.width = pct + \"%\";\n                    progressPercent.textContent = pct + \"%\";\n                    progressText.textContent = \"Scanned version \" + completed + \" of \" + toScan.length + \"...\";\n                }\n            }));\n        }<\/p>\n<p>        progressText.textContent = \"Scan complete!\";<\/p>\n<p>        if (!allResults.length) {\n            resultsEl.innerHTML = \"<\/p>\n<div style='background:var(--bg-body); padding:25px; border-radius:6px; border:1px solid var(--border); text-align:center; color:var(--text-secondary);'>No matches found for <strong>\" + esc(query) + \"<\/strong> across scanned versions.<\/div>\n<p>\";\n            return;\n        }<\/p>\n<p>        allResults.sort((a, b) => Number(b.versionId) - Number(a.versionId));<\/p>\n<p>        const cardsHtml = allResults.map((res) => {\n            const matchRows = res.matches.slice(0, 6).map((m) => [\n                \"<\/p>\n<div style='background:#0b1114; border:1px solid var(--border); padding:10px; border-radius:4px; margin-top:8px; font-family:monospace; font-size:0.85rem; overflow-x:auto;'>\",\n                \"  <span style='color:#ff922b; display:block; margin-bottom:4px;'>\" + esc(m.path) + \"<\/span>\",\n                \"  <span style='color:#e0e6ed; white-space:pre-wrap;'>\" + esc(m.snippet.slice(0, 400)) + (m.snippet.length > 400 ? \"...\" : \"\") + \"<\/span>\",\n                \"<\/div>\n<p>\"\n            ].join(\"\")).join(\"\");<\/p>\n<p>            return [\n                \"<\/p>\n<div style='background:var(--bg-body); border:1px solid var(--border); border-radius:8px; padding:18px; margin-bottom:15px;'>\",\n                \"  <\/p>\n<div style='display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px; border-bottom:1px solid var(--border); padding-bottom:12px; margin-bottom:12px;'>\",\n                \"    <\/p>\n<div style='display:flex; align-items:center; gap:10px;'>\",\n                \"      \" + getBadge(res.type),\n                \"      <strong style='color:var(--text-primary); font-size:1.05rem;'>\" + esc(res.entityName) + \"<\/strong>\",\n                \"    <\/div>\n<p>\",\n                \"    <\/p>\n<div style='color:var(--text-secondary); font-size:0.85rem;'>\",\n                \"      Found in Version: <strong style='color:var(--accent);'>v\" + esc(res.versionId) + \"<\/strong> (\" + esc(res.versionName) + \")\",\n                \"    <\/div>\n<p>\",\n                \"  <\/p><\/div>\n<p>\",\n                \"  <\/p>\n<div style='color:var(--text-secondary); font-size:0.85rem;'>Matched Properties (\" + res.matches.length + \"):<\/div>\n<p>\",\n                \"  \" + matchRows,\n                \"<\/p><\/div>\n<p>\"\n            ].join(\"\");\n        }).join(\"\");<\/p>\n<p>        resultsEl.innerHTML = [\n            \"<\/p>\n<h3 style='color:var(--text-primary); margin-bottom:15px; font-size:1.15rem;'>Found \" + allResults.length + \" Historical Match(es)<\/h3>\n<p>\",\n            cardsHtml\n        ].join(\"\");\n    });<\/p>\n<p>    \/\/ 5. CHANGE HISTORY OF ONE ELEMENT (23.10.2026)\n    \/\/ Tracks a tag, trigger, variable etc. by its ID across the versions in the chosen range.\n    \/\/ Each version is reduced to normalized element strings; identical strings are shared, so a\n    \/\/ second element of the same container is answered from memory without new API calls.\n    const T = {\"types\": {\"tag\": \"Tag\", \"trigger\": \"Trigger\", \"variable\": \"Variable\", \"builtInVariable\": \"Built-in variable\", \"customTemplate\": \"Template\", \"folder\": \"Folder\", \"client\": \"Client\", \"transformation\": \"Transformation\", \"zone\": \"Zone\", \"gtagConfig\": \"Google tag config\"}, \"loadingList\": \"Loading the element list of the newest version ...\", \"listReady\": \"Element list from version v{v} loaded \u2013 suggestions appear while typing.\", \"urlOtherContainer\": \"Note: the pasted URL belongs to a different account or container than the one selected.\", \"moreLines\": \"\u2026 {n} more lines\", \"fromLine\": \"from line {n}\", \"absent\": \"not set\", \"before\": \"Before\", \"after\": \"After\", \"moreFields\": \"\u2026 {n} more fields (complete list in the JSON export)\", \"depsTitle\": \"Referenced elements\", \"depAdded\": \"now referenced\", \"depRemoved\": \"no longer referenced\", \"depChanged\": \"changed\", \"depMissing\": \"missing\", \"depNotInContainer\": \"not present in this version\", \"fieldsChanged\": \"{n} field(s) differ\", \"openInGtm\": \"open version in GTM\", \"live\": \"Live\", \"ev_baseline\": \"Present in the oldest scanned version\", \"ev_created\": \"Created\", \"ev_restored\": \"Present again\", \"ev_removed\": \"Removed\", \"ev_changed\": \"Configuration changed\", \"ev_renamed\": \"Renamed from \u201c{from}\u201d to \u201c{to}\u201d\", \"ev_depChanged\": \"Unchanged itself \u2013 a referenced element changed\", \"ev_touched\": \"Saved again without a configuration difference\", \"ev_draft\": \"Workspace \u201c{ws}\u201d: {status}, not yet published\", \"k_baseline\": \"Start of range\", \"k_created\": \"Created\", \"k_restored\": \"Restored\", \"k_removed\": \"Removed\", \"k_changed\": \"Changed\", \"k_renamed\": \"Renamed\", \"k_depChanged\": \"Dependency\", \"k_touched\": \"Saved\", \"k_draft\": \"Draft\", \"ws_added\": \"added\", \"ws_deleted\": \"deleted\", \"ws_updated\": \"changed\", \"ws_none\": \"unchanged\", \"draftCompared\": \"{n} field(s) differ from version v{v}\", \"version\": \"Version\", \"versionDate\": \"Version saved\", \"elementDate\": \"Element last saved\", \"versionNotes\": \"Version notes\", \"absentSince\": \"absent since the version after v{v}\", \"gapNote\": \"{n} version(s) before this one could not be read\", \"showConfig\": \"Show configuration ({n} fields)\", \"account\": \"GTM account\", \"container\": \"Container\", \"scanned\": \"Scanned\", \"scannedText\": \"{n} versions ({from} to {to})\", \"unreadable\": \"{n} version(s) could not be read\", \"rangeHint\": \"Older versions are outside the chosen range; \u201cALL Versions\u201d in step 2 covers the complete history.\", \"liveVersion\": \"Live version\", \"noLive\": \"none published\", \"draftError\": \"Workspace changes could not be read:\", \"exportJson\": \"Export JSON\", \"exportCsv\": \"Export CSV\", \"elementId\": \"ID\", \"firstSeen\": \"First seen\", \"beforeRange\": \"created in this version or earlier\", \"lastChange\": \"Last change\", \"noChange\": \"no change in the range\", \"changeCount\": \"Changes in the range\", \"inLive\": \"Part of the live version\", \"yes\": \"yes\", \"no\": \"no\", \"liveOutOfRange\": \"live version outside the range\", \"names\": \"Names over time\", \"limits\": \"Limits: the GTM API returns no author for an element or a version \u2013 who published a version is shown in the version list of the Tag Manager interface. Dates are derived from the fingerprints the API returns for versions and elements; Google documents them as change markers, and they currently contain the save time in milliseconds. A version records the configuration at the moment it was created, so edits made and reverted between two versions leave no trace. Google-delivered runtime code, container-wide settings and permissions are not part of this comparison. Everything runs in this browser tab; nothing is sent to lukaswojcik.com.\", \"multiMatch\": \"The entry matches {n} elements; each one has its own history below.\", \"needQuery\": \"Please enter a name, an ID or a GTM URL.\", \"noVersions\": \"This container has no versions yet.\", \"progress\": \"Reading version {n} of {total} ...\", \"authExpired\": \"The Google sign-in has expired (HTTP 401). Please authorize again in step 1.\", \"nothingReadable\": \"None of the versions in the range could be read.\", \"notFound\": \"No element named or numbered \u201c{q}\u201d was found in the {n} scanned versions.\", \"similar\": \"Similar names in the newest version:\", \"loadingDrafts\": \"Checking workspaces for unpublished changes ...\", \"tooMany\": \"{n} elements match; the first 10 are shown. A more exact name, an ID or a type narrows the result.\"};\n    const EH_LOCALE = \"en-GB\";\n    const EH_TYPES = {\n        tag: \"tagId\", trigger: \"triggerId\", variable: \"variableId\", builtInVariable: \"type\",\n        customTemplate: \"templateId\", folder: \"folderId\", client: \"clientId\",\n        transformation: \"transformationId\", zone: \"zoneId\", gtagConfig: \"gtagConfigId\"\n    };\n    const EH_URL_TYPES = { tags: \"tag\", triggers: \"trigger\", variables: \"variable\", templates: \"customTemplate\", folders: \"folder\", clients: \"client\", transformations: \"transformation\", zones: \"zone\" };\n    const EH_IGNORE = { fingerprint: 1, path: 1, tagManagerUrl: 1, accountId: 1, containerId: 1, workspaceId: 1 };\n    const EH_EVENT_COLORS = { created: \"var(--ok)\", restored: \"var(--ok)\", baseline: \"var(--text-secondary)\", changed: \"var(--accent)\", renamed: \"var(--accent)\", removed: \"var(--fail)\", depChanged: \"var(--lw-tool-warn)\", touched: \"var(--text-secondary)\", draft: \"var(--lw-tool-warn)\" };<\/p>\n<p>    const ehPanel = document.getElementById(\"element-history-panel\");\n    const ehResults = document.getElementById(\"element-history-results\");\n    const ehTabs = document.getElementById(\"gtm-mode-tabs\");\n    const ehType = document.getElementById(\"eh-type\");\n    const ehQuery = document.getElementById(\"eh-query\");\n    const ehList = document.getElementById(\"eh-suggestions\");\n    const ehRunBtn = document.getElementById(\"eh-run-btn\");\n    const ehStatus = document.getElementById(\"eh-status\");\n    const ehProg = document.getElementById(\"eh-progress\");\n    const ehProgBar = document.getElementById(\"eh-progress-bar\");\n    const ehProgText = document.getElementById(\"eh-progress-text\");\n    const ehProgPct = document.getElementById(\"eh-progress-percent\");<\/p>\n<p>    let ehMode = \"search\";\n    let ehCache = new Map();\n    let ehIntern = new Map();\n    let ehLiveId = null;\n    let ehNewest = null;\n    let ehGen = 0;\n    let ehLast = null;<\/p>\n<p>    const ehAttr = (s) => esc(s).replace(\/\"\/g, \"&quot;\").replace(\/'\/g, \"&#39;\");\n    const ehFmt = (s, o) => String(s).replace(\/\\{(\\w+)\\}\/g, (m, k) => (o && k in o ? o[k] : m));\n    const ehTypeName = (t) => T.types[t] || t;\n    const ehNumSort = (a, b) => Number(a.id) - Number(b.id);<\/p>\n<p>    const ehDate = (fp) => {\n        if (!\/^\\d{13}$\/.test(String(fp || \"\"))) return \"\";\n        const n = Number(fp);\n        if (n < 1325376000000 || n > Date.now() + 86400000) return \"\";\n        try { return new Date(n).toLocaleString(EH_LOCALE, { dateStyle: \"medium\", timeStyle: \"short\" }); }\n        catch (e) { return new Date(n).toISOString(); }\n    };<\/p>\n<p>    \/\/ Parameter lists [{type,key,value}] become objects keyed by \"key\", so a diff names\n    \/\/ \"parameter.html.value\" instead of a position that shifts when a parameter is added.\n    const ehNorm = (v, top) => {\n        if (Array.isArray(v)) {\n            const keyed = v.length > 0 && v.every(x => x && typeof x === \"object\" && !Array.isArray(x) && typeof x.key === \"string\");\n            if (keyed && new Set(v.map(x => x.key)).size === v.length) {\n                const o = {};\n                v.slice().sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)).forEach(x => {\n                    const item = Object.assign({}, x);\n                    delete item.key;\n                    o[x.key] = ehNorm(item, false);\n                });\n                return o;\n            }\n            return v.map(x => ehNorm(x, false));\n        }\n        if (v && typeof v === \"object\") {\n            const o = {};\n            Object.keys(v).sort().forEach(k => { if (!(top && EH_IGNORE[k])) o[k] = ehNorm(v[k], false); });\n            return o;\n        }\n        return v;\n    };<\/p>\n<p>    const ehFlat = (v, p, out) => {\n        if (v && typeof v === \"object\") {\n            const isArr = Array.isArray(v);\n            const ks = isArr ? v.map((x, i) => i) : Object.keys(v);\n            if (!ks.length) { out.set(p || \"value\", isArr ? \"[]\" : \"{}\"); return out; }\n            ks.forEach(k => ehFlat(v[k], isArr ? p + \"[\" + k + \"]\" : (p ? p + \".\" + k : k), out));\n        } else {\n            out.set(p || \"value\", v === undefined || v === null ? \"\" : String(v));\n        }\n        return out;\n    };<\/p>\n<p>    const ehRecord = (vh, ver) => {\n        const rec = {\n            id: String(vh.containerVersionId), name: ver.name || vh.name || \"\", desc: ver.description || \"\",\n            fp: ver.fingerprint || \"\", url: ver.tagManagerUrl || \"\", ok: true, items: {}\n        };\n        Object.keys(EH_TYPES).forEach(type => {\n            const list = ver[type] || [];\n            const m = new Map();\n            list.forEach(it => {\n                const id = String(it[EH_TYPES[type]] == null ? \"\" : it[EH_TYPES[type]]);\n                if (!id) return;\n                const j = JSON.stringify(ehNorm(it, true));\n                const f = String(it.fingerprint || \"\");\n                const key = f + \"\\u0000\" + j;\n                let entry = ehIntern.get(key);\n                if (!entry) { entry = { n: String(it.name || \"\"), f: f, j: j }; ehIntern.set(key, entry); }\n                m.set(id, entry);\n            });\n            rec.items[type] = m;\n        });\n        return rec;\n    };<\/p>\n<p>    const ehFetch = async (vh, gen) => {\n        if (ehCache.has(vh.path)) return ehCache.get(vh.path);\n        let rec;\n        try {\n            const ver = await gtmApi(vh.path);\n            rec = ehRecord(vh, ver);\n            if (gen === ehGen) ehCache.set(vh.path, rec);\n        } catch (e) {\n            rec = { id: String(vh.containerVersionId), name: vh.name || \"\", ok: false, err: e.message, items: {} };\n        }\n        return rec;\n    };<\/p>\n<p>    const ehShowMode = (mode) => {\n        ehMode = mode;\n        const history = mode === \"history\";\n        document.getElementById(\"search-controls-panel\").style.display = history ? \"none\" : \"block\";\n        resultsEl.style.display = history ? \"none\" : \"block\";\n        ehPanel.style.display = history ? \"block\" : \"none\";\n        ehResults.style.display = history ? \"block\" : \"none\";\n        ehTabs.querySelectorAll(\".gtm-mode-tab\").forEach(btn => {\n            const active = btn.getAttribute(\"data-mode\") === mode;\n            btn.setAttribute(\"aria-selected\", active ? \"true\" : \"false\");\n            btn.style.background = active ? \"var(--accent)\" : \"transparent\";\n            btn.style.color = active ? \"var(--on-accent)\" : \"var(--accent)\";\n        });\n    };\n    ehTabs.querySelectorAll(\".gtm-mode-tab\").forEach(btn => btn.addEventListener(\"click\", () => ehShowMode(btn.getAttribute(\"data-mode\"))));<\/p>\n<p>    const ehFillList = () => {\n        ehList.innerHTML = \"\";\n        if (!ehNewest) return;\n        const sel = ehType.value;\n        const types = sel === \"any\" ? Object.keys(EH_TYPES) : [sel];\n        const seen = new Set();\n        let count = 0;\n        types.forEach(t => {\n            const m = ehNewest.items[t];\n            if (!m) return;\n            m.forEach((it, id) => {\n                if (count >= 3000 || !it.n || seen.has(t + it.n)) return;\n                seen.add(t + it.n);\n                const opt = document.createElement(\"option\");\n                opt.value = it.n;\n                opt.label = ehTypeName(t) + (t === \"builtInVariable\" ? \"\" : \" \u00b7 ID \" + id);\n                ehList.appendChild(opt);\n                count++;\n            });\n        });\n    };\n    ehType.addEventListener(\"change\", ehFillList);<\/p>\n<p>    \/\/ Called by the container selector once the version headers are loaded.\n    const ehContainerReady = async () => {\n        const gen = ++ehGen;\n        ehCache = new Map();\n        ehIntern = new Map();\n        ehLiveId = null;\n        ehNewest = null;\n        ehLast = null;\n        ehResults.innerHTML = \"\";\n        ehList.innerHTML = \"\";\n        ehTabs.style.display = \"flex\";\n        ehShowMode(ehMode);\n        const base = \"accounts\/\" + accSelect.value + \"\/containers\/\" + contSelect.value;\n        try {\n            const live = await gtmApi(base + \"\/versions:live\");\n            if (gen === ehGen && live && live.containerVersionId != null) ehLiveId = String(live.containerVersionId);\n        } catch (e) { \/* container was never published *\/ }\n        if (gen !== ehGen || !versionHeaders.length) return;\n        ehStatus.textContent = T.loadingList;\n        let rec = null;\n        for (let i = 0; i < Math.min(3, versionHeaders.length) &#038;&#038; !(rec &#038;&#038; rec.ok); i++) {\n            rec = await ehFetch(versionHeaders[i], gen);\n            if (gen !== ehGen) return;\n        }\n        if (rec.ok) {\n            ehNewest = rec;\n            ehFillList();\n            ehStatus.textContent = ehFmt(T.listReady, { v: rec.id });\n        } else {\n            ehStatus.textContent = \"\";\n        }\n    };\n\n    const ehParse = (raw) => {\n        const q = raw.trim();\n        const r = { q: q, type: ehType.value, id: null, warn: \"\" };\n        const m = q.match(\/\\\/(tags|triggers|variables|templates|folders|clients|transformations|zones)\\\/(\\d+)\/);\n        if (m) {\n            r.type = EH_URL_TYPES[m[1]];\n            r.id = m[2];\n            const c = q.match(\/accounts\\\/(\\d+)\\\/containers\\\/(\\d+)\/);\n            if (c && (c[1] !== accSelect.value || c[2] !== contSelect.value)) r.warn = T.urlOtherContainer;\n        }\n        return r;\n    };<\/p>\n<p>    const ehByName = (rec, type) => {\n        rec._byName = rec._byName || {};\n        if (!rec._byName[type]) {\n            const m = new Map();\n            (rec.items[type] || new Map()).forEach((it, id) => { if (!m.has(it.n)) m.set(it.n, id); });\n            rec._byName[type] = m;\n        }\n        return rec._byName[type];\n    };<\/p>\n<p>    \/\/ Triggers of a tag, the template behind a cvt_ type and every {{variable}}, followed\n    \/\/ through variables and triggers until no new reference appears.\n    const ehDeps = (rec, type, id, entry) => {\n        const out = new Map();\n        const queue = [{ t: type, e: entry }];\n        const add = (dt, did, label) => {\n            const key = dt + \":\" + did;\n            if (out.has(key) || (dt === type && did === id)) return;\n            const d = rec.items[dt] && rec.items[dt].get(did);\n            out.set(key, d ? { t: dt, id: did, n: d.n, j: d.j } : { t: dt, id: did, n: label || did, j: null });\n            if (d && dt !== \"builtInVariable\") queue.push({ t: dt, e: d });\n        };\n        while (queue.length) {\n            const cur = queue.shift();\n            let obj;\n            try { obj = JSON.parse(cur.e.j); } catch (e) { continue; }\n            if (cur.t === \"tag\") {\n                [].concat(obj.firingTriggerId || [], obj.blockingTriggerId || []).forEach(x => add(\"trigger\", String(x)));\n            }\n            if (\/^cvt_\/.test(String(obj.type || \"\"))) {\n                (rec.items.customTemplate || new Map()).forEach((tpl, tid) => { if (String(obj.type).endsWith(\"_\" + tid)) add(\"customTemplate\", tid); });\n            }\n            const re = \/\\{\\{([^{}]+)\\}\\}\/g;\n            let m;\n            while ((m = re.exec(cur.e.j))) {\n                const name = m[1].trim();\n                const vid = ehByName(rec, \"variable\").get(name);\n                if (vid) { add(\"variable\", vid); continue; }\n                const bid = ehByName(rec, \"builtInVariable\").get(name);\n                if (bid) { add(\"builtInVariable\", bid); continue; }\n                add(\"variable\", \"?\" + name, name);\n            }\n        }\n        return out;\n    };<\/p>\n<p>    const ehDiff = (aj, bj) => {\n        const a = ehFlat(aj ? JSON.parse(aj) : {}, \"\", new Map());\n        const b = ehFlat(bj ? JSON.parse(bj) : {}, \"\", new Map());\n        const rows = [];\n        new Set([...a.keys(), ...b.keys()]).forEach(p => {\n            const x = a.has(p) ? a.get(p) : null;\n            const y = b.has(p) ? b.get(p) : null;\n            if (x !== y) rows.push({ path: p, before: x, after: y });\n        });\n        return rows.sort((r1, r2) => (r1.path < r2.path ? -1 : r1.path > r2.path ? 1 : 0));\n    };<\/p>\n<p>    const ehDepDiff = (a, b) => {\n        const out = [];\n        new Set([...a.keys(), ...b.keys()]).forEach(k => {\n            const x = a.get(k), y = b.get(k);\n            if (x && !y) out.push({ kind: \"depRemoved\", t: x.t, id: x.id, n: x.n, missing: !x.j });\n            else if (!x && y) out.push({ kind: \"depAdded\", t: y.t, id: y.id, n: y.n, missing: !y.j });\n            else if (x.j !== y.j) {\n                if (!y.j) out.push({ kind: \"depMissing\", t: y.t, id: y.id, n: y.n, missing: true });\n                else out.push({ kind: \"depChanged\", t: y.t, id: y.id, n: y.n, nBefore: x.n, rows: ehDiff(x.j, y.j) });\n            }\n        });\n        return out;\n    };<\/p>\n<p>    const ehTimeline = (recs, type, id, opts) => {\n        const events = [];\n        let prev = null;\n        let lastPresent = null;\n        let gap = 0;\n        recs.forEach((rec, idx) => {\n            if (!rec.ok) { gap++; return; }\n            const cur = (rec.items[type] && rec.items[type].get(id)) || null;\n            const deps = cur && opts.deps ? ehDeps(rec, type, id, cur) : null;\n            const base = { version: rec, entry: cur, gap: gap, rows: [], deps: [] };\n            if (!prev) {\n                if (cur) events.push(Object.assign(base, { kind: \"baseline\", rows: ehDiff(null, cur.j) }));\n            } else if (!prev.cur && cur) {\n                if (lastPresent) events.push(Object.assign(base, { kind: \"restored\", rows: ehDiff(lastPresent.cur.j, cur.j), since: lastPresent.rec, deps: deps && lastPresent.deps ? ehDepDiff(lastPresent.deps, deps) : [] }));\n                else events.push(Object.assign(base, { kind: \"created\", rows: ehDiff(null, cur.j) }));\n            } else if (prev.cur && !cur) {\n                events.push(Object.assign(base, { kind: \"removed\", entry: prev.cur }));\n            } else if (prev.cur && cur) {\n                const rows = prev.cur.j !== cur.j ? ehDiff(prev.cur.j, cur.j) : [];\n                const depRows = deps && prev.deps ? ehDepDiff(prev.deps, deps) : [];\n                const renamed = prev.cur.n !== cur.n;\n                if (rows.length || depRows.length) {\n                    events.push(Object.assign(base, {\n                        kind: renamed ? \"renamed\" : rows.length ? \"changed\" : \"depChanged\",\n                        rows: rows, deps: depRows, oldName: renamed ? prev.cur.n : \"\"\n                    }));\n                } else if (opts.touched && prev.cur.f !== cur.f) {\n                    events.push(Object.assign(base, { kind: \"touched\" }));\n                }\n            }\n            if (cur) lastPresent = { cur: cur, rec: rec, deps: deps };\n            prev = { cur: cur, deps: deps, rec: rec };\n            gap = 0;\n        });\n        return events;\n    };<\/p>\n<p>    const ehDrafts = async (keys, newestRec) => {\n        const out = [];\n        const base = \"accounts\/\" + accSelect.value + \"\/containers\/\" + contSelect.value;\n        const ws = await gtmApi(base + \"\/workspaces\");\n        for (const w of (ws.workspace || [])) {\n            const st = await gtmApi(w.path + \"\/status\");\n            (st.workspaceChange || []).forEach(ch => {\n                Object.keys(EH_TYPES).forEach(t => {\n                    if (!ch[t]) return;\n                    const id = String(ch[t][EH_TYPES[t]] == null ? \"\" : ch[t][EH_TYPES[t]]);\n                    if (!keys.has(t + \":\" + id)) return;\n                    const before = newestRec && newestRec.items[t] && newestRec.items[t].get(id);\n                    const j = JSON.stringify(ehNorm(ch[t], true));\n                    out.push({\n                        kind: \"draft\", t: t, id: id, workspace: w.name || w.workspaceId, status: ch.changeStatus || \"\",\n                        fpEntity: ch[t].fingerprint || \"\", name: ch[t].name || \"\",\n                        rows: ch.changeStatus === \"deleted\" ? [] : ehDiff(before ? before.j : null, j)\n                    });\n                });\n            });\n        }\n        return out;\n    };<\/p>\n<p>    \/\/ Line diff for code fields (Custom HTML, template code): common start and end are cut off,\n    \/\/ the middle is compared line by line.\n    const ehLineDiff = (a, b) => {\n        const A = a.split(\"\\n\"), B = b.split(\"\\n\");\n        let s = 0;\n        while (s < A.length &#038;&#038; s < B.length &#038;&#038; A[s] === B[s]) s++;\n        let e = 0;\n        while (e < A.length - s &#038;&#038; e < B.length - s &#038;&#038; A[A.length - 1 - e] === B[B.length - 1 - e]) e++;\n        const x = A.slice(s, A.length - e), y = B.slice(s, B.length - e);\n        let ops = [];\n        if (x.length * y.length <= 250000) {\n            const n = x.length, m = y.length, w = m + 1;\n            const L = new Uint32Array((n + 1) * w);\n            for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) {\n                L[i * w + j] = x[i] === y[j] ? L[(i + 1) * w + j + 1] + 1 : Math.max(L[(i + 1) * w + j], L[i * w + j + 1]);\n            }\n            let i = 0, j = 0;\n            while (i < n &#038;&#038; j < m) {\n                if (x[i] === y[j]) { ops.push([\" \", x[i]]); i++; j++; }\n                else if (L[(i + 1) * w + j] >= L[i * w + j + 1]) { ops.push([\"-\", x[i]]); i++; }\n                else { ops.push([\"+\", y[j]]); j++; }\n            }\n            while (i < n) ops.push([\"-\", x[i++]]);\n            while (j < m) ops.push([\"+\", y[j++]]);\n        } else {\n            ops = x.map(l => [\"-\", l]).concat(y.map(l => [\"+\", l]));\n        }\n        const before = A.slice(Math.max(0, s - 2), s).map(l => [\" \", l]);\n        const after = A.slice(A.length - e, A.length - e + 2).map(l => [\" \", l]);\n        return { start: Math.max(0, s - 2) + 1, ops: before.concat(ops, after) };\n    };<\/p>\n<p>    const ehMark = (a, b) => {\n        let s = 0;\n        while (s < a.length &#038;&#038; s < b.length &#038;&#038; a[s] === b[s]) s++;\n        let e = 0;\n        while (e < a.length - s &#038;&#038; e < b.length - s &#038;&#038; a[a.length - 1 - e] === b[b.length - 1 - e]) e++;\n        const part = (str, color) => esc(str.slice(0, s)) + \"<mark style='background:\" + color + \"; color:inherit; border-radius:2px;'>\" + esc(str.slice(s, str.length - e)) + \"<\/mark>\" + esc(str.slice(str.length - e));\n        return [part(a, \"color-mix(in srgb, var(--fail) 30%, transparent)\"), part(b, \"color-mix(in srgb, var(--ok) 30%, transparent)\")];\n    };<\/p>\n<p>    const ehRefName = (path, value, rec) => {\n        if (value == null || value === \"\" || !rec) return \"\";\n        let t = null;\n        if (\/(^|\\.)(firingTriggerId|blockingTriggerId)\\[\\d+\\]$\/.test(path)) t = \"trigger\";\n        else if (\/(^|\\.)parentFolderId$\/.test(path)) t = \"folder\";\n        if (t) {\n            const it = rec.items[t] && rec.items[t].get(value);\n            return it ? it.n : \"\";\n        }\n        if (path === \"type\" && \/^cvt_\/.test(value)) {\n            let n = \"\";\n            (rec.items.customTemplate || new Map()).forEach((tpl, tid) => { if (value.endsWith(\"_\" + tid)) n = tpl.n; });\n            return n;\n        }\n        return \"\";\n    };<\/p>\n<p>    const ehRowsHtml = (rows, recBefore, recAfter) => {\n        if (!rows.length) return \"\";\n        const box = \"background:var(--bg-panel); border:1px solid var(--border); border-radius:4px; padding:6px 8px; font-family:monospace; font-size:0.82rem; white-space:pre-wrap; overflow-wrap:anywhere; margin:0;\";\n        return rows.slice(0, 200).map(r => {\n            const head = \"<\/p>\n<div style='color:var(--accent); font-family:monospace; font-size:0.8rem; margin:10px 0 4px; overflow-wrap:anywhere;'>\" + esc(r.path) + \"<\/div>\n<p>\";\n            const bRef = ehRefName(r.path, r.before, recBefore), aRef = ehRefName(r.path, r.after, recAfter);\n            const ref = (n) => (n ? \" <span style='color:var(--text-secondary);'>(\" + esc(n) + \")<\/span>\" : \"\");\n            const before = r.before == null ? null : r.before, after = r.after == null ? null : r.after;\n            if (before != null && after != null && (before.indexOf(\"\\n\") >= 0 || after.indexOf(\"\\n\") >= 0)) {\n                const d = ehLineDiff(before, after);\n                const lines = d.ops.slice(0, 400).map(op => {\n                    const bg = op[0] === \"-\" ? \"color-mix(in srgb, var(--fail) 18%, transparent)\" : op[0] === \"+\" ? \"color-mix(in srgb, var(--ok) 18%, transparent)\" : \"transparent\";\n                    return \"<\/p>\n<div style='background:\" + bg + \";'>\" + esc(op[0] + \" \" + op[1]) + \"<\/div>\n<p>\";\n                }).join(\"\");\n                const more = d.ops.length > 400 ? \"<\/p>\n<div style='color:var(--text-secondary);'>\" + esc(ehFmt(T.moreLines, { n: d.ops.length - 400 })) + \"<\/div>\n<p>\" : \"\";\n                return head + \"<\/p>\n<div style='color:var(--text-secondary); font-size:0.78rem; margin-bottom:3px;'>\" + esc(ehFmt(T.fromLine, { n: d.start })) + \"<\/div>\n<div style='\" + box + \" white-space:pre; overflow-x:auto; overflow-wrap:normal;'>\" + lines + more + \"<\/div>\n<p>\";\n            }\n            let bHtml, aHtml;\n            if (before != null && after != null) {\n                const cut = (s) => (s.length > 2000 ? s.slice(0, 2000) + \" \u2026\" : s);\n                [bHtml, aHtml] = ehMark(cut(before), cut(after));\n            } else {\n                const cut = (s) => esc(s.length > 2000 ? s.slice(0, 2000) + \" \u2026\" : s);\n                bHtml = before == null ? \"<em style='color:var(--text-secondary);'>\" + esc(T.absent) + \"<\/em>\" : cut(before);\n                aHtml = after == null ? \"<em style='color:var(--text-secondary);'>\" + esc(T.absent) + \"<\/em>\" : cut(after);\n            }\n            return head + \"<\/p>\n<div style='display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:6px;'>\" +\n                \"<\/p>\n<div>\n<div style='color:var(--fail); font-size:0.75rem; font-weight:700;'>\" + esc(T.before) + \"<\/div>\n<pre style='\" + box + \"'>\" + bHtml + ref(bRef) + \"<\/pre>\n<\/div>\n<p>\" +\n                \"<\/p>\n<div>\n<div style='color:var(--ok); font-size:0.75rem; font-weight:700;'>\" + esc(T.after) + \"<\/div>\n<pre style='\" + box + \"'>\" + aHtml + ref(aRef) + \"<\/pre>\n<\/div>\n<\/div>\n<p>\";\n        }).join(\"\") + (rows.length > 200 ? \"<\/p>\n<div style='color:var(--text-secondary); margin-top:8px;'>\" + esc(ehFmt(T.moreFields, { n: rows.length - 200 })) + \"<\/div>\n<p>\" : \"\");\n    };<\/p>\n<p>    \/\/ Full configuration of a new element: one line per field instead of before\/after pairs.\n    const ehConfigHtml = (rows, rec) => \"<\/p>\n<div style='background:var(--bg-panel); border:1px solid var(--border); border-radius:4px; padding:8px 10px; margin-top:8px; font-family:monospace; font-size:0.82rem; overflow-x:auto;'>\" +\n        rows.map(r => {\n            const v = r.after == null ? \"\" : r.after;\n            const n = ehRefName(r.path, v, rec);\n            return \"<\/p>\n<div style='display:flex; gap:12px; flex-wrap:wrap; padding:3px 0; border-bottom:1px solid var(--border);'><span style='color:var(--accent); min-width:220px;'>\" + esc(r.path) + \"<\/span><span style='white-space:pre-wrap; overflow-wrap:anywhere; flex:1; min-width:180px; color:var(--text-primary);'>\" + esc(v.length > 4000 ? v.slice(0, 4000) + \" \u2026\" : v) + (n ? \" <span style='color:var(--text-secondary);'>(\" + esc(n) + \")<\/span>\" : \"\") + \"<\/span><\/div>\n<p>\";\n        }).join(\"\") + \"<\/p><\/div>\n<p>\";<\/p>\n<p>    const ehBadge = (text, color) => \"<span style='display:inline-block; border:1px solid \" + color + \"; color:\" + color + \"; padding:2px 8px; border-radius:4px; font-size:0.72rem; font-weight:700; letter-spacing:0.04em; text-transform:uppercase;'>\" + esc(text) + \"<\/span>\";<\/p>\n<p>    const ehDepHtml = (deps, recBefore, recAfter) => {\n        if (!deps.length) return \"\";\n        return \"<\/p>\n<div style='margin-top:14px; border-top:1px dashed var(--border); padding-top:10px;'>\n<div style='color:var(--text-primary); font-weight:700; font-size:0.88rem;'>\" + esc(T.depsTitle) + \"<\/div>\n<p>\" +\n            deps.map(d => {\n                const label = ehTypeName(d.t) + \" \u00b7 \" + (d.n || d.id) + (d.id.charAt(0) === \"?\" || d.t === \"builtInVariable\" ? \"\" : \" (ID \" + d.id + \")\");\n                const text = T[d.kind];\n                const inner = d.kind === \"depChanged\"\n                    ? \"<\/p>\n<details style='margin-top:4px;'>\n<summary style='cursor:pointer; color:var(--text-secondary); font-size:0.85rem;'>\" + esc(ehFmt(T.fieldsChanged, { n: d.rows.length })) + \"<\/summary>\n<p>\" + ehRowsHtml(d.rows, recBefore, recAfter) + \"<\/details>\n<p>\"\n                    : \"\";\n                return \"<\/p>\n<div style='margin-top:8px; font-size:0.88rem; color:var(--text-secondary);'>\" + ehBadge(text, d.kind === \"depAdded\" ? \"var(--ok)\" : d.kind === \"depChanged\" ? \"var(--lw-tool-warn)\" : \"var(--fail)\") + \" <strong style='color:var(--text-primary);'>\" + esc(label) + \"<\/strong>\" + (d.missing ? \" \u2013 \" + esc(T.depNotInContainer) : \"\") + inner + \"<\/div>\n<p>\";\n            }).join(\"\") + \"<\/p><\/div>\n<p>\";\n    };<\/p>\n<p>    const ehVersionLink = (rec) => (\/^https:\\\/\\\/tagmanager\\.google\\.com\\\/\/.test(rec.url || \"\") ? \" \u00b7 <a href='\" + ehAttr(rec.url) + \"' target='_blank' rel='noopener noreferrer'>\" + esc(T.openInGtm) + \"<\/a>\" : \"\");<\/p>\n<p>    const ehEventHtml = (ev, prevRecOf) => {\n        const rec = ev.version;\n        const color = EH_EVENT_COLORS[ev.kind];\n        const verDate = ehDate(rec.fp);\n        const entDate = ev.entry && ev.kind !== \"removed\" ? ehDate(ev.entry.f) : \"\";\n        const live = ehLiveId === rec.id ? \" \" + ehBadge(T.live, \"var(--ok)\") : \"\";\n        const title = ev.kind === \"renamed\" ? ehFmt(T.ev_renamed, { from: ev.oldName, to: ev.entry.n }) : T[\"ev_\" + ev.kind];\n        const meta = [];\n        if (verDate) meta.push(esc(T.versionDate) + \": <strong style='color:var(--text-primary);'>\" + esc(verDate) + \"<\/strong>\");\n        if (entDate) meta.push(esc(T.elementDate) + \": \" + esc(entDate));\n        if (ev.since) meta.push(esc(ehFmt(T.absentSince, { v: ev.since.id })));\n        if (ev.gap) meta.push(\"<span style='color:var(--lw-tool-warn);'>\" + esc(ehFmt(T.gapNote, { n: ev.gap })) + \"<\/span>\");\n        const rowsHtml = ev.rows.length\n            ? (ev.kind === \"baseline\" || ev.kind === \"created\"\n                ? \"<\/p>\n<details style='margin-top:10px;'>\n<summary style='cursor:pointer; color:var(--text-secondary); font-size:0.85rem;'>\" + esc(ehFmt(T.showConfig, { n: ev.rows.length })) + \"<\/summary>\n<p>\" + ehConfigHtml(ev.rows, rec) + \"<\/details>\n<p>\"\n                : \"<\/p>\n<div style='color:var(--text-secondary); font-size:0.85rem; margin-top:10px;'>\" + esc(ehFmt(T.fieldsChanged, { n: ev.rows.length })) + \"<\/div>\n<p>\" + ehRowsHtml(ev.rows, prevRecOf(rec), rec))\n            : \"\";\n        return \"<\/p>\n<div style='background:var(--bg-body); border:1px solid var(--border); border-left:3px solid \" + color + \"; border-radius:8px; padding:16px 18px; margin-bottom:12px;'>\" +\n            \"<\/p>\n<div style='display:flex; justify-content:space-between; gap:10px; flex-wrap:wrap; align-items:center;'>\" +\n            \"<\/p>\n<div style='display:flex; gap:10px; align-items:center; flex-wrap:wrap;'>\" + ehBadge(T[\"k_\" + ev.kind], color) + \"<strong style='color:var(--text-primary);'>\" + esc(title) + \"<\/strong><\/div>\n<p>\" +\n            \"<\/p>\n<div style='color:var(--text-secondary); font-size:0.85rem;'>\" + esc(T.version) + \" <strong style='color:var(--accent);'>v\" + esc(rec.id) + \"<\/strong>\" + (rec.name ? \" (\" + esc(rec.name) + \")\" : \"\") + live + ehVersionLink(rec) + \"<\/div>\n<\/div>\n<p>\" +\n            (meta.length ? \"<\/p>\n<div style='color:var(--text-secondary); font-size:0.84rem; margin-top:8px; display:flex; gap:6px 16px; flex-wrap:wrap;'>\" + meta.map(x => \"<span>\" + x + \"<\/span>\").join(\"\") + \"<\/div>\n<p>\" : \"\") +\n            (rec.desc ? \"<\/p>\n<div style='color:var(--text-secondary); font-size:0.84rem; margin-top:6px; white-space:pre-wrap; overflow-wrap:anywhere;'><strong>\" + esc(T.versionNotes) + \":<\/strong> \" + esc(rec.desc.slice(0, 1000)) + \"<\/div>\n<p>\" : \"\") +\n            rowsHtml + ehDepHtml(ev.deps || [], prevRecOf(rec), rec) + \"<\/p><\/div>\n<p>\";\n    };<\/p>\n<p>    const ehDraftHtml = (d) => {\n        const color = EH_EVENT_COLORS.draft;\n        const entDate = ehDate(d.fpEntity);\n        const st = T[\"ws_\" + d.status] || d.status;\n        return \"<\/p>\n<div style='background:var(--bg-body); border:1px dashed \" + color + \"; border-radius:8px; padding:16px 18px; margin-bottom:12px;'>\" +\n            \"<\/p>\n<div style='display:flex; gap:10px; align-items:center; flex-wrap:wrap;'>\" + ehBadge(T.k_draft, color) + \"<strong style='color:var(--text-primary);'>\" + esc(ehFmt(T.ev_draft, { ws: d.workspace, status: st })) + \"<\/strong><\/div>\n<p>\" +\n            (entDate ? \"<\/p>\n<div style='color:var(--text-secondary); font-size:0.84rem; margin-top:8px;'>\" + esc(T.elementDate) + \": \" + esc(entDate) + \"<\/div>\n<p>\" : \"\") +\n            (d.rows.length ? \"<\/p>\n<div style='color:var(--text-secondary); font-size:0.85rem; margin-top:10px;'>\" + esc(ehFmt(T.draftCompared, { n: d.rows.length, v: ehLast.newest.id })) + \"<\/div>\n<p>\" + ehRowsHtml(d.rows, ehLast.newest, ehLast.newest) : \"\") +\n            \"<\/p><\/div>\n<p>\";\n    };<\/p>\n<p>    const ehDownload = (name, mime, text) => {\n        const url = URL.createObjectURL(new Blob([text], { type: mime }));\n        const a = document.createElement(\"a\");\n        a.href = url;\n        a.download = name;\n        document.body.appendChild(a);\n        a.click();\n        a.remove();\n        setTimeout(() => URL.revokeObjectURL(url), 2000);\n    };<\/p>\n<p>    const ehExportData = () => {\n        const L = ehLast;\n        return {\n            tool: \"GTM element change history \u2013 lukaswojcik.com\",\n            created: new Date().toISOString(),\n            account: L.account, container: L.container, liveVersion: ehLiveId,\n            scannedVersions: L.scanned, unreadableVersions: L.unreadable,\n            elements: L.histories.map(h => ({\n                type: h.type, id: h.id, name: h.name, names: h.names,\n                events: h.events.map(ev => ({\n                    event: ev.kind, version: ev.version.id, versionName: ev.version.name, versionNotes: ev.version.desc,\n                    versionSaved: ehDate(ev.version.fp), elementSaved: ev.entry ? ehDate(ev.entry.f) : \"\",\n                    previousName: ev.oldName || undefined, unreadableVersionsBefore: ev.gap || undefined,\n                    fields: ev.rows, dependencies: (ev.deps || []).map(d => ({ change: d.kind, type: d.t, id: d.id, name: d.n, fields: d.rows || [] }))\n                })),\n                drafts: h.drafts.map(d => ({ workspace: d.workspace, status: d.status, elementSaved: ehDate(d.fpEntity), fields: d.rows }))\n            }))\n        };\n    };<\/p>\n<p>    const ehCsv = () => {\n        const q = (v) => '\"' + String(v == null ? \"\" : v).replace(\/\"\/g, '\"\"') + '\"';\n        const lines = [[\"element_type\", \"element_id\", \"element_name\", \"version\", \"version_name\", \"version_saved\", \"event\", \"field\", \"before\", \"after\"].map(q).join(\",\")];\n        ehLast.histories.forEach(h => {\n            h.events.forEach(ev => {\n                const base = [h.type, h.id, ev.entry ? ev.entry.n : h.name, ev.version.id, ev.version.name, ehDate(ev.version.fp), ev.kind];\n                const rows = ev.rows.length ? ev.rows : [{ path: \"\", before: \"\", after: \"\" }];\n                rows.forEach(r => lines.push(base.concat([r.path, r.before, r.after]).map(q).join(\",\")));\n                (ev.deps || []).forEach(d => {\n                    (d.rows && d.rows.length ? d.rows : [{ path: \"\", before: \"\", after: \"\" }]).forEach(r => {\n                        lines.push(base.concat([d.kind + \" \" + d.t + \":\" + d.id + \" \" + d.n + (r.path ? \" \u2192 \" + r.path : \"\"), r.before, r.after]).map(q).join(\",\"));\n                    });\n                });\n            });\n            h.drafts.forEach(d => {\n                (d.rows.length ? d.rows : [{ path: \"\", before: \"\", after: \"\" }]).forEach(r => {\n                    lines.push([h.type, h.id, d.name || h.name, \"workspace\", d.workspace, ehDate(d.fpEntity), \"draft_\" + d.status, r.path, r.before, r.after].map(q).join(\",\"));\n                });\n            });\n        });\n        return \"\ufeff\" + lines.join(\"\\r\\n\");\n    };<\/p>\n<p>    const ehRender = () => {\n        const L = ehLast;\n        const rangeNote = L.isAll ? \"\" : \" \" + esc(T.rangeHint);\n        const summaryHead = \"<\/p>\n<div style='background:var(--bg-body); border:1px solid var(--border); border-radius:8px; padding:16px 18px; margin-bottom:18px; color:var(--text-secondary); font-size:0.9rem; line-height:1.6;'>\" +\n            \"<\/p>\n<div><strong style='color:var(--text-primary);'>\" + esc(T.account) + \":<\/strong> \" + esc(L.account.name) + \" (\" + esc(L.account.id) + \") \u00b7 <strong style='color:var(--text-primary);'>\" + esc(T.container) + \":<\/strong> \" + esc(L.container.name) + \" (\" + esc(L.container.publicId) + \")<\/div>\n<p>\" +\n            \"<\/p>\n<div><strong style='color:var(--text-primary);'>\" + esc(T.scanned) + \":<\/strong> \" + esc(ehFmt(T.scannedText, { n: L.scanned, from: L.from, to: L.to })) + (L.unreadable ? \" \u00b7 <span style='color:var(--lw-tool-warn);'>\" + esc(ehFmt(T.unreadable, { n: L.unreadable })) + \"<\/span>\" : \"\") + rangeNote + \"<\/div>\n<p>\" +\n            \"<\/p>\n<div><strong style='color:var(--text-primary);'>\" + esc(T.liveVersion) + \":<\/strong> \" + (ehLiveId ? \"v\" + esc(ehLiveId) : esc(T.noLive)) + \"<\/div>\n<p>\" +\n            (L.warn ? \"<\/p>\n<div style='color:var(--lw-tool-warn);'>\" + esc(L.warn) + \"<\/div>\n<p>\" : \"\") +\n            (L.draftError ? \"<\/p>\n<div style='color:var(--lw-tool-warn);'>\" + esc(T.draftError) + \" \" + esc(L.draftError) + \"<\/div>\n<p>\" : \"\") +\n            \"<\/p>\n<div style='margin-top:10px; display:flex; gap:10px; flex-wrap:wrap;'>\" +\n            \"<button type='button' id='eh-export-json' class='button' style='background:transparent; color:var(--accent); border:1px solid var(--accent); padding:7px 14px; border-radius:6px; cursor:pointer; font-weight:700;'>\" + esc(T.exportJson) + \"<\/button>\" +\n            \"<button type='button' id='eh-export-csv' class='button' style='background:transparent; color:var(--accent); border:1px solid var(--accent); padding:7px 14px; border-radius:6px; cursor:pointer; font-weight:700;'>\" + esc(T.exportCsv) + \"<\/button><\/div>\n<\/div>\n<p>\";<\/p>\n<p>        const okRecs = L.recs.filter(r => r.ok);\n        const prevRecOf = (rec) => { const i = okRecs.indexOf(rec); return i > 0 ? okRecs[i - 1] : null; };<\/p>\n<p>        const blocks = L.histories.map(h => {\n            const changes = h.events.filter(ev => ev.kind !== \"baseline\" && ev.kind !== \"touched\");\n            const lastChange = changes.length ? changes[changes.length - 1] : null;\n            const first = h.events[0];\n            const inLive = ehLiveId && L.recs.some(r => r.ok && r.id === ehLiveId && r.items[h.type] && r.items[h.type].has(h.id));\n            const facts = [\n                [T.elementId, h.id],\n                [T.firstSeen, first ? \"v\" + first.version.id + (first.kind === \"baseline\" ? \" \u2013 \" + T.beforeRange : \"\") + (ehDate(first.version.fp) ? \" \u00b7 \" + ehDate(first.version.fp) : \"\") : \"\u2013\"],\n                [T.lastChange, lastChange ? \"v\" + lastChange.version.id + \" \u00b7 \" + T[\"k_\" + lastChange.kind] + (ehDate(lastChange.version.fp) ? \" \u00b7 \" + ehDate(lastChange.version.fp) : \"\") : T.noChange],\n                [T.changeCount, String(changes.length)],\n                [T.inLive, ehLiveId ? (L.liveScanned ? (inLive ? T.yes : T.no) : T.liveOutOfRange) : \"\u2013\"]\n            ];\n            if (h.names.length > 1) facts.push([T.names, h.names.join(\" \u2192 \")]);\n            const head = \"<\/p>\n<div style='margin:22px 0 12px;'>\n<div style='display:flex; gap:10px; align-items:center; flex-wrap:wrap;'>\" + ehBadge(ehTypeName(h.type), \"var(--accent)\") + \"<strong style='color:var(--text-primary); font-size:1.1rem;'>\" + esc(h.name) + \"<\/strong><\/div>\n<p>\" +\n                \"<\/p>\n<div style='display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:8px; margin-top:10px;'>\" +\n                facts.map(f => \"<\/p>\n<div style='background:var(--bg-body); border:1px solid var(--border); border-radius:6px; padding:8px 10px;'>\n<div style='color:var(--text-secondary); font-size:0.75rem;'>\" + esc(f[0]) + \"<\/div>\n<div style='color:var(--text-primary); font-size:0.9rem; overflow-wrap:anywhere;'>\" + esc(f[1]) + \"<\/div>\n<\/div>\n<p>\").join(\"\") +\n                \"<\/p><\/div>\n<\/div>\n<p>\";\n            const drafts = h.drafts.map(ehDraftHtml).join(\"\");\n            const evs = h.events.slice().reverse().map(ev => ehEventHtml(ev, prevRecOf)).join(\"\");\n            return head + drafts + evs;\n        }).join(\"\");<\/p>\n<p>        const note = \"<\/p>\n<div style='color:var(--text-secondary); font-size:0.82rem; line-height:1.55; margin-top:18px; border-top:1px solid var(--border); padding-top:12px;'>\" + esc(T.limits) + \"<\/div>\n<p>\";\n        ehResults.innerHTML = summaryHead + (L.histories.length > 1 ? \"<\/p>\n<div style='color:var(--lw-tool-warn); font-size:0.9rem;'>\" + esc(ehFmt(T.multiMatch, { n: L.histories.length })) + \"<\/div>\n<p>\" : \"\") + blocks + note;\n        const stamp = new Date().toISOString().slice(0, 10);\n        const fname = \"gtm-element-history-\" + (L.container.publicId || \"container\") + \"-\" + stamp;\n        document.getElementById(\"eh-export-json\").addEventListener(\"click\", () => ehDownload(fname + \".json\", \"application\/json\", JSON.stringify(ehExportData(), null, 2)));\n        document.getElementById(\"eh-export-csv\").addEventListener(\"click\", () => ehDownload(fname + \".csv\", \"text\/csv;charset=utf-8\", ehCsv()));\n    };<\/p>\n<p>    const ehRun = async () => {\n        const raw = ehQuery.value;\n        if (!raw.trim()) { ehStatus.textContent = T.needQuery; ehQuery.focus(); return; }\n        if (!versionHeaders.length) { ehStatus.textContent = T.noVersions; return; }\n        const parsed = ehParse(raw);\n        const gen = ehGen;\n        const rangeVal = rangeSelect.value;\n        const toScan = rangeVal === \"all\" ? versionHeaders : versionHeaders.slice(0, Number(rangeVal));\n        const opts = { deps: document.getElementById(\"eh-deps\").checked, drafts: document.getElementById(\"eh-drafts\").checked, touched: document.getElementById(\"eh-touched\").checked };<\/p>\n<p>        ehRunBtn.disabled = true;\n        ehResults.innerHTML = \"\";\n        ehStatus.textContent = \"\";\n        const missing = toScan.filter(vh => !ehCache.has(vh.path));\n        const recsByPath = new Map();\n        toScan.forEach(vh => { if (ehCache.has(vh.path)) recsByPath.set(vh.path, ehCache.get(vh.path)); });\n        let authFailed = false;\n        if (missing.length) {\n            ehProg.style.display = \"block\";\n            let done = 0;\n            const concurrency = 3;\n            for (let i = 0; i < missing.length &#038;&#038; !authFailed; i += concurrency) {\n                await Promise.all(missing.slice(i, i + concurrency).map(async vh => {\n                    const rec = await ehFetch(vh, gen);\n                    if (!rec.ok && \/\\b401\\b\/.test(rec.err || \"\")) authFailed = true;\n                    recsByPath.set(vh.path, rec);\n                    done++;\n                    const pct = Math.round(done \/ missing.length * 100);\n                    ehProgBar.style.width = pct + \"%\";\n                    ehProgPct.textContent = pct + \"%\";\n                    ehProgText.textContent = ehFmt(T.progress, { n: done, total: missing.length });\n                }));\n                if (gen !== ehGen) { ehRunBtn.disabled = false; ehProg.style.display = \"none\"; return; }\n            }\n            ehProg.style.display = \"none\";\n        }\n        ehRunBtn.disabled = false;\n        if (authFailed) { ehStatus.innerHTML = \"<span style='color:var(--fail);'>\" + esc(T.authExpired) + \"<\/span>\"; return; }<\/p>\n<p>        const recs = toScan.map(vh => recsByPath.get(vh.path) || { id: String(vh.containerVersionId), ok: false, items: {} }).sort(ehNumSort);\n        const okRecs = recs.filter(r => r.ok);\n        if (!okRecs.length) { ehStatus.innerHTML = \"<span style='color:var(--fail);'>\" + esc(T.nothingReadable) + \"<\/span>\"; return; }<\/p>\n<p>        const types = parsed.type === \"any\" ? Object.keys(EH_TYPES) : [parsed.type];\n        const lc = parsed.q.toLowerCase();\n        const keys = new Map();\n        okRecs.forEach(rec => types.forEach(t => {\n            const m = rec.items[t];\n            if (!m) return;\n            if (parsed.id) { if (m.has(parsed.id)) keys.set(t + \":\" + parsed.id, { t: t, id: parsed.id }); return; }\n            m.forEach((it, id) => {\n                if (it.n.toLowerCase() === lc || id === parsed.q || (t === \"builtInVariable\" && id.toLowerCase() === lc)) keys.set(t + \":\" + id, { t: t, id: id });\n            });\n        }));<\/p>\n<p>        if (!keys.size) {\n            const sugg = [];\n            const newest = okRecs[okRecs.length - 1];\n            types.forEach(t => (newest.items[t] || new Map()).forEach((it, id) => {\n                if (sugg.length < 12 &#038;&#038; lc &#038;&#038; it.n.toLowerCase().indexOf(lc) >= 0) sugg.push(ehTypeName(t) + \": \" + it.n);\n            }));\n            ehStatus.innerHTML = \"\";\n            ehResults.innerHTML = \"<\/p>\n<div style='background:var(--bg-body); padding:22px; border-radius:6px; border:1px solid var(--border); color:var(--text-secondary);'>\" +\n                esc(ehFmt(T.notFound, { q: parsed.q, n: okRecs.length })) + (parsed.warn ? \"<br \/>\" + esc(parsed.warn) : \"\") +\n                (sugg.length ? \"<\/p>\n<div style='margin-top:10px;'>\" + esc(T.similar) + \"<\/div>\n<ul style='margin:6px 0 0;'>\" + sugg.map(s => \"<\/p>\n<li>\" + esc(s) + \"<\/li>\n<p>\").join(\"\") + \"<\/ul>\n<p>\" : \"\") + \"<\/p><\/div>\n<p>\";\n            return;\n        }<\/p>\n<p>        const list = Array.from(keys.values()).slice(0, 10);\n        const histories = list.map(k => {\n            const events = ehTimeline(recs, k.t, k.id, opts);\n            const names = [];\n            okRecs.forEach(r => { const it = r.items[k.t] && r.items[k.t].get(k.id); if (it && names[names.length - 1] !== it.n) names.push(it.n); });\n            return { type: k.t, id: k.id, name: names[names.length - 1] || k.id, names: names, events: events, drafts: [] };\n        });<\/p>\n<p>        let draftError = \"\";\n        if (opts.drafts) {\n            ehStatus.textContent = T.loadingDrafts;\n            try {\n                const drafts = await ehDrafts(new Set(list.map(k => k.t + \":\" + k.id)), okRecs[okRecs.length - 1]);\n                drafts.forEach(d => { const h = histories.find(x => x.type === d.t && x.id === d.id); if (h) h.drafts.push(d); });\n            } catch (e) { draftError = e.message; }\n            if (gen !== ehGen) return;\n        }<\/p>\n<p>        const cont = containersMap[contSelect.value] || {};\n        ehLast = {\n            recs: recs, histories: histories, newest: okRecs[okRecs.length - 1], warn: parsed.warn, draftError: draftError,\n            account: { id: accSelect.value, name: accountsMap[accSelect.value] || \"\" },\n            container: { id: contSelect.value, name: cont.name || \"\", publicId: cont.publicId || \"\" },\n            scanned: okRecs.length, unreadable: recs.length - okRecs.length, from: \"v\" + okRecs[0].id, to: \"v\" + okRecs[okRecs.length - 1].id,\n            isAll: rangeVal === \"all\" || toScan.length === versionHeaders.length,\n            liveScanned: !!ehLiveId && okRecs.some(r => r.id === ehLiveId)\n        };\n        ehStatus.textContent = keys.size > 10 ? ehFmt(T.tooMany, { n: keys.size }) : \"\";\n        ehRender();\n    };<\/p>\n<p>    ehRunBtn.addEventListener(\"click\", ehRun);\n    ehQuery.addEventListener(\"keydown\", (e) => { if (e.key === \"Enter\") { e.preventDefault(); ehRun(); } });\n})();\n<\/script><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Searches selected lists of tags, triggers, variables, templates and folders in retrieved GTM versions and shows the change history of a single element. Runtime behavior, central settings and permissions need a separate review.<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":38,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"template-tool-base.php","meta":{"footnotes":""},"tags":[91267],"class_list":["post-8812","page","type-page","status-publish","hentry","tag-google-tag-manager"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>GTM Container History &amp; Deep Search Engine | Lukas Wojcik<\/title>\n<meta name=\"description\" content=\"Search GTM version lists or follow one tag, trigger or variable across versions with Google sign-in. Configuration history does not establish runtime behavior.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/gtm-container-history-deep-search-engine\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"GTM Container History &amp; Deep Search Engine | Lukas Wojcik\" \/>\n<meta property=\"og:description\" content=\"Search selected GTM version lists with Google sign-in. Configuration history does not establish runtime behavior or restore permissions.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/gtm-container-history-deep-search-engine\/\" \/>\n<meta property=\"og:site_name\" content=\"Lukas Wojcik - Blog\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-23T13:00:33+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/08\/og-default.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:description\" content=\"Search selected GTM version lists with Google sign-in. Configuration history does not establish runtime behavior or restore permissions.\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"3 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/gtm-container-history-deep-search-engine\\\/\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/gtm-container-history-deep-search-engine\\\/\",\"name\":\"GTM Container History &amp; Deep Search Engine | Lukas Wojcik\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\"},\"datePublished\":\"2026-08-05T13:36:34+00:00\",\"dateModified\":\"2026-10-23T13:00:33+00:00\",\"description\":\"Search GTM version lists or follow one tag, trigger or variable across versions with Google sign-in. Configuration history does not establish runtime behavior.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/gtm-container-history-deep-search-engine\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/gtm-container-history-deep-search-engine\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/gtm-container-history-deep-search-engine\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Toolbox\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"GTM Container History &amp; Deep Search Engine\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\",\"name\":\"Lukas Wojcik - Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\",\"name\":\"Lukas Wojcik\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"width\":424,\"height\":636,\"caption\":\"Lukas Wojcik\"},\"logo\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\"},\"sameAs\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"GTM Container History &amp; Deep Search Engine | Lukas Wojcik","description":"Search GTM version lists or follow one tag, trigger or variable across versions with Google sign-in. Configuration history does not establish runtime behavior.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/gtm-container-history-deep-search-engine\/","og_locale":"en_US","og_type":"article","og_title":"GTM Container History &amp; Deep Search Engine | Lukas Wojcik","og_description":"Search selected GTM version lists with Google sign-in. Configuration history does not establish runtime behavior or restore permissions.","og_url":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/gtm-container-history-deep-search-engine\/","og_site_name":"Lukas Wojcik - Blog","article_modified_time":"2026-10-23T13:00:33+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/08\/og-default.jpg","type":"image\/jpeg"}],"twitter_card":"summary_large_image","twitter_description":"Search selected GTM version lists with Google sign-in. Configuration history does not establish runtime behavior or restore permissions.","twitter_misc":{"Est. reading time":"3 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/gtm-container-history-deep-search-engine\/","url":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/gtm-container-history-deep-search-engine\/","name":"GTM Container History &amp; Deep Search Engine | Lukas Wojcik","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#website"},"datePublished":"2026-08-05T13:36:34+00:00","dateModified":"2026-10-23T13:00:33+00:00","description":"Search GTM version lists or follow one tag, trigger or variable across versions with Google sign-in. Configuration history does not establish runtime behavior.","breadcrumb":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/gtm-container-history-deep-search-engine\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/gtm-container-history-deep-search-engine\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/gtm-container-history-deep-search-engine\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.lukaswojcik.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Toolbox","item":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/"},{"@type":"ListItem","position":3,"name":"GTM Container History &amp; Deep Search Engine"}]},{"@type":"WebSite","@id":"https:\/\/www.lukaswojcik.com\/blog\/#website","url":"https:\/\/www.lukaswojcik.com\/blog\/","name":"Lukas Wojcik - Blog","description":"","publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.lukaswojcik.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9","name":"Lukas Wojcik","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","width":424,"height":636,"caption":"Lukas Wojcik"},"logo":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg"},"sameAs":["https:\/\/www.lukaswojcik.com\/blog"]}]}},"_links":{"self":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/8812","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/comments?post=8812"}],"version-history":[{"count":7,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/8812\/revisions"}],"predecessor-version":[{"id":27559,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/8812\/revisions\/27559"}],"up":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/38"}],"wp:attachment":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media?parent=8812"}],"wp:term":[{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/tags?post=8812"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}