<all_urls>
// ============================================
// GA4/GTM ДИАГНОСТИКА v5.1 (FIXED)
// ТОЛЬКО НАБЛЮДЕНИЕ — НЕ ЛОМАЕТ ОТПРАВКУ
// ============================================
(function() {
if (document.getElementById('ga4-diagnostic-panel')) {
console.log('ℹ️ Панель диагностики уже активна');
return;
}
const STORAGE_KEY = '__ga4_diagnostic_data';
// ========== СПРАВОЧНИК ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ ==========
const REQUIRED_FIELDS = {
'view_item': { fields: ['items'], itemFields: ['item_id', 'item_name'] },
'view_item_list': { fields: ['items'], itemFields: ['item_id', 'item_name'] },
'select_item': { fields: ['items'], itemFields: ['item_id', 'item_name'] },
'add_to_cart': { fields: ['items', 'currency', 'value'], itemFields: ['item_id', 'item_name', 'price', 'quantity'] },
'remove_from_cart': { fields: ['items', 'currency', 'value'], itemFields: ['item_id', 'item_name', 'price', 'quantity'] },
'add_to_wishlist': { fields: ['items', 'currency', 'value'], itemFields: ['item_id', 'item_name', 'price'] },
'view_cart': { fields: ['items', 'currency', 'value'], itemFields: ['item_id', 'item_name', 'price', 'quantity'] },
'begin_checkout': { fields: ['items', 'currency', 'value'], itemFields: ['item_id', 'item_name', 'price', 'quantity'] },
'add_shipping_info': { fields: ['items', 'currency', 'value'], itemFields: ['item_id', 'item_name'] },
'add_payment_info': { fields: ['items', 'currency', 'value'], itemFields: ['item_id', 'item_name'] },
'purchase': { fields: ['transaction_id', 'value', 'currency', 'items'], itemFields: ['item_id', 'item_name', 'price', 'quantity'] },
'view_promotion': { fields: ['items'], itemFields: ['item_id', 'item_name'] },
'select_promotion': { fields: ['items'], itemFields: ['item_id', 'item_name'] }
};
// ========== СОЗДАЕМ ПАНЕЛЬ ==========
const panel = document.createElement('div');
panel.id = 'ga4-diagnostic-panel';
panel.innerHTML = `
<style>
#ga4-diagnostic-panel {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 999999;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace;
font-size: 12px;
background: #1e1e2f;
color: #fff;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
width: 450px;
display: flex;
flex-direction: column;
overflow: hidden;
transition: all 0.3s ease;
}
#ga4-diagnostic-panel.collapsed { width: auto; min-width: 200px; }
#ga4-diagnostic-header {
background: linear-gradient(135deg, #4a4e69, #2d2d3a);
padding: 10px 14px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
user-select: none;
}
#ga4-diagnostic-header:hover { background: linear-gradient(135deg, #6c6f8d, #3a3a48); }
#ga4-diagnostic-title { font-size: 13px; display: flex; align-items: center; gap: 8px; }
#ga4-diagnostic-status-light {
width: 10px;
height: 10px;
border-radius: 50%;
background: #4caf50;
box-shadow: 0 0 5px #4caf50;
animation: pulse 2s infinite;
}
@keyframes pulse { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
#ga4-diagnostic-content {
padding: 12px;
overflow-y: auto;
max-height: 600px;
background: #2d2d3a;
font-size: 11px;
}
.ga4-diagnostic-btn {
background: #6c6f8d;
border: none;
color: white;
padding: 6px 12px;
margin: 2px;
border-radius: 6px;
cursor: pointer;
font-size: 11px;
font-family: inherit;
transition: all 0.2s;
}
.ga4-diagnostic-btn:hover { background: #8b8eae; transform: scale(1.02); }
.ga4-diagnostic-btn.primary { background: #4caf50; }
.ga4-diagnostic-btn.danger { background: #c44536; }
.ga4-diagnostic-stats {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.ga4-diagnostic-stat {
background: #3a3a48;
padding: 6px 10px;
border-radius: 8px;
text-align: center;
flex: 1;
min-width: 60px;
}
.ga4-diagnostic-stat-value { font-size: 18px; font-weight: bold; color: #4caf50; }
.ga4-diagnostic-stat-label { font-size: 9px; opacity: 0.7; }
.ga4-diagnostic-events-list { max-height: 300px; overflow-y: auto; margin: 8px 0; }
.ga4-diagnostic-event {
padding: 8px;
margin: 6px 0;
border-left: 3px solid #4caf50;
background: #3a3a48;
border-radius: 8px;
font-size: 10px;
cursor: pointer;
transition: all 0.2s;
}
.ga4-diagnostic-event.valid { border-left-color: #4caf50; }
.ga4-diagnostic-event.invalid { border-left-color: #f44336; background: #4a2a2a; }
.ga4-diagnostic-event.warning { border-left-color: #ff9800; background: #4a3a2a; }
.ga4-diagnostic-badge {
border-radius: 10px;
padding: 2px 6px;
font-size: 9px;
margin-left: 6px;
display: inline-block;
}
.ga4-diagnostic-badge.error { background: #c44536; }
.ga4-diagnostic-badge.warning { background: #ff9800; }
.ga4-diagnostic-badge.success { background: #4caf50; }
.ga4-diagnostic-error-list { color: #ff6b6b; font-size: 9px; margin-top: 4px; }
.ga4-download-menu { position: relative; display: inline-block; }
.ga4-download-content {
display: none;
position: absolute;
bottom: 100%;
left: 0;
background: #3a3a48;
min-width: 160px;
border-radius: 8px;
padding: 4px 0;
margin-bottom: 4px;
z-index: 1000;
}
.ga4-download-content button {
display: block;
width: 100%;
text-align: left;
background: none;
border: none;
color: white;
padding: 6px 12px;
cursor: pointer;
font-size: 11px;
}
.ga4-download-content button:hover { background: #6c6f8d; }
.ga4-download-menu:hover .ga4-download-content { display: block; }
hr { border-color: #4a4e69; margin: 8px 0; }
.ga4-section-title { font-size: 11px; font-weight: bold; margin: 8px 0 4px 0; color: #aaa; }
.ga4-funnel { background: #1e1e2f; border-radius: 8px; padding: 8px; margin: 8px 0; }
.ga4-funnel-step { display: flex; align-items: center; gap: 8px; margin: 4px 0; padding: 4px; border-radius: 4px; }
.ga4-funnel-step.completed { color: #4caf50; }
.ga4-funnel-step.missing { color: #c44536; opacity: 0.6; }
.ga4-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
z-index: 1000000;
display: flex;
align-items: center;
justify-content: center;
}
.ga4-modal-content {
color: #f5f5f5;
background: #2d2d3a;
border-radius: 12px;
max-width: 550px;
max-height: 80vh;
overflow: auto;
padding: 20px;
font-family: monospace;
font-size: 11px;
}
</style>
<div id="ga4-diagnostic-header">
<div id="ga4-diagnostic-title">
<span id="ga4-diagnostic-status-light"></span>
<span>🔍 GA4 Диагностика v5.1</span>
</div>
<div style="display: flex; gap: 8px;">
<span id="ga4-error-count" style="background:#c44536; border-radius:10px; padding:2px 6px; font-size:10px;">0</span>
<span id="ga4-diagnostic-collapse-icon">▼</span>
</div>
</div>
<div id="ga4-diagnostic-content">
<div class="ga4-diagnostic-stats" id="ga4-diagnostic-stats">
<div class="ga4-diagnostic-stat"><div class="ga4-diagnostic-stat-value" id="stat-pages">0</div><div class="ga4-diagnostic-stat-label">страниц</div></div>
<div class="ga4-diagnostic-stat"><div class="ga4-diagnostic-stat-value" id="stat-events">0</div><div class="ga4-diagnostic-stat-label">событий</div></div>
<div class="ga4-diagnostic-stat"><div class="ga4-diagnostic-stat-value" id="stat-errors">0</div><div class="ga4-diagnostic-stat-label">ошибок</div></div>
<div class="ga4-diagnostic-stat"><div class="ga4-diagnostic-stat-value" id="stat-warnings">0</div><div class="ga4-diagnostic-stat-label">⚠️</div></div>
</div>
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px;">
<button class="ga4-diagnostic-btn primary" id="ga4-btn-report">📊 Отчет</button>
<div class="ga4-download-menu">
<button class="ga4-diagnostic-btn" id="ga4-btn-download">💾 Скачать ▼</button>
<div class="ga4-download-content">
<button id="ga4-download-json">📄 JSON</button>
<button id="ga4-download-csv">📊 CSV</button>
<button id="ga4-download-txt">📝 TXT</button>
<button id="ga4-download-html">🌐 HTML</button>
<button id="ga4-download-validation">🔍 Ошибки</button>
</div>
</div>
<button class="ga4-diagnostic-btn danger" id="ga4-btn-reset">🗑️ Сброс</button>
</div>
<div class="ga4-section-title">🎯 Воронка покупки</div>
<div id="ga4-funnel" class="ga4-funnel"></div>
<div class="ga4-section-title">📡 События</div>
<div id="ga4-diagnostic-events" class="ga4-diagnostic-events-list"></div>
<hr>
<div class="ga4-section-title">⚠️ Ошибки</div>
<div id="ga4-diagnostic-log" class="ga4-diagnostic-log"></div>
</div>
`;
document.body.appendChild(panel);
// ========== СОСТОЯНИЕ ==========
let diagnosticData = {
sessionId: Date.now() + '_' + Math.random().toString(36).substr(2, 6),
startTime: new Date().toISOString(),
lastUpdate: new Date().toISOString(),
pages: [],
events: [],
errors: [],
warnings: [],
gtmId: null,
ga4Id: null,
funnel: {
view_item_list: false,
select_item: false,
view_item: false,
add_to_cart: false,
begin_checkout: false,
purchase: false
}
};
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
const parsed = JSON.parse(saved);
diagnosticData = parsed;
console.log(`📂 Загружена сессия: ${diagnosticData.events.length} событий`);
}
} catch(e) {}
const currentUrl = window.location.href;
if (!diagnosticData.pages.some(p => p.url === currentUrl)) {
diagnosticData.pages.push({ url: currentUrl, title: document.title, timestamp: new Date().toISOString() });
saveData();
}
function saveData() {
diagnosticData.lastUpdate = new Date().toISOString();
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(diagnosticData)); } catch(e) {}
updateUI();
}
// ========== ВАЛИДАЦИЯ ==========
function validateEvent(eventName, data) {
const errors = [];
const warnings = [];
const eventData = data.ecommerce || data;
const items = eventData.items || [];
const required = REQUIRED_FIELDS[eventName];
if (required) {
required.fields.forEach(field => {
if (eventData[field] === undefined || eventData[field] === null) {
errors.push(`Отсутствует обязательное поле: ${field}`);
} else if (field === 'items' && (!Array.isArray(eventData[field]) || eventData[field].length === 0)) {
errors.push(`Поле items должно быть непустым массивом`);
}
});
if (items.length > 0) {
items.forEach((item, idx) => {
const hasId = item.item_id !== undefined && item.item_id !== '';
const hasName = item.item_name !== undefined && item.item_name !== '';
if (!hasId && !hasName) {
errors.push(`Товар #${idx+1}: нет ни item_id, ни item_name`);
}
if (required.itemFields) {
required.itemFields.forEach(field => {
if (field !== 'item_id' && field !== 'item_name' && item[field] === undefined) {
warnings.push(`Товар #${idx+1}: отсутствует поле ${field}`);
}
});
}
});
}
}
// Проверка суммы
if (eventData.value !== undefined && items.length > 0) {
const sum = items.reduce((acc, item) => acc + (item.price || 0) * (item.quantity || 1), 0);
if (Math.abs(sum - eventData.value) > 0.01) {
warnings.push(`value (${eventData.value}) != сумме товаров (${sum})`);
}
}
return { errors, warnings };
}
function checkGlobalConsistency() {
const globalWarnings = [];
const currencies = new Set();
diagnosticData.events.forEach(e => {
const currency = e.data.currency || e.data.ecommerce?.currency;
if (currency) currencies.add(currency);
});
if (currencies.size > 1) globalWarnings.push(`⚠️ Несколько валют: ${[...currencies].join(', ')}`);
const hasPurchase = diagnosticData.events.some(e => e.name === 'purchase');
const hasBeginCheckout = diagnosticData.events.some(e => e.name === 'begin_checkout');
if (hasPurchase && !hasBeginCheckout) globalWarnings.push(`⚠️ purchase без begin_checkout`);
return globalWarnings;
}
function updateFunnel() {
const steps = ['view_item_list', 'select_item', 'view_item', 'add_to_cart', 'begin_checkout', 'purchase'];
steps.forEach(step => { diagnosticData.funnel[step] = diagnosticData.events.some(e => e.name === step); });
const funnelDiv = document.getElementById('ga4-funnel');
if (funnelDiv) {
funnelDiv.innerHTML = steps.map(step => {
const completed = diagnosticData.funnel[step];
return `<div class="ga4-funnel-step ${completed ? 'completed' : 'missing'}">${completed ? '✅' : '⬜'} ${step}</div>`;
}).join('');
}
}
// ========== ДОБАВЛЕНИЕ СОБЫТИЯ (БЕЗ ПЕРЕХВАТА) ==========
function addEvent(eventName, eventData, source) {
const { errors, warnings } = validateEvent(eventName, eventData);
const event = {
name: eventName,
data: eventData,
source: source,
url: window.location.href,
timestamp: new Date().toISOString(),
validation: { isValid: errors.length === 0, errors, warnings }
};
diagnosticData.events.push(event);
errors.forEach(err => {
diagnosticData.errors.push({ type: 'error', event: eventName, message: err, timestamp: event.timestamp });
});
warnings.forEach(warn => {
diagnosticData.warnings.push({ type: 'warning', event: eventName, message: warn, timestamp: event.timestamp });
});
checkGlobalConsistency().forEach(warn => {
if (!diagnosticData.warnings.some(w => w.message === warn)) {
diagnosticData.warnings.push({ type: 'global', event: null, message: warn, timestamp: new Date().toISOString() });
}
});
updateFunnel();
saveData();
const color = !event.validation.isValid ? '#f44336' : (warnings.length > 0 ? '#ff9800' : '#4caf50');
console.log(`%c📡 ${eventName} [${event.validation.isValid ? '✓' : '✗'}]`, `color: ${color}`, eventData);
if (errors.length) console.groupCollapsed(`%c❌ Ошибки (${errors.length})`, 'color: #f44336');
errors.forEach(e => console.log(` ${e}`));
if (errors.length) console.groupEnd();
if (warnings.length) console.groupCollapsed(`%c⚠️ Предупреждения (${warnings.length})`, 'color: #ff9800');
warnings.forEach(w => console.log(` ${w}`));
if (warnings.length) console.groupEnd();
}
function updateUI() {
document.getElementById('stat-pages').textContent = diagnosticData.pages.length;
document.getElementById('stat-events').textContent = diagnosticData.events.length;
document.getElementById('stat-errors').textContent = diagnosticData.errors.length;
document.getElementById('stat-warnings').textContent = diagnosticData.warnings.length;
document.getElementById('ga4-error-count').textContent = diagnosticData.errors.length;
const eventsDiv = document.getElementById('ga4-diagnostic-events');
const recentEvents = [...diagnosticData.events].reverse().slice(0, 15);
eventsDiv.innerHTML = recentEvents.map(e => {
const isValid = e.validation.isValid;
const hasWarnings = e.validation.warnings.length > 0;
let statusClass = 'valid';
if (!isValid) statusClass = 'invalid';
else if (hasWarnings) statusClass = 'warning';
const transId = e.data.transaction_id || e.data.ecommerce?.transaction_id;
const value = e.data.value || e.data.ecommerce?.value;
const itemName = e.data.items?.[0]?.item_name || '';
const statusIcon = !isValid ? '🔴' : (hasWarnings ? '🟠' : '✅');
return `
<div class="ga4-diagnostic-event ${statusClass}" onclick="window.showEventDetails(${JSON.stringify(e).replace(/"/g, '"')})">
<div style="display: flex; justify-content: space-between;">
<strong>${statusIcon} ${e.name}</strong>
<span style="color:#8b8eae;">${new Date(e.timestamp).toLocaleTimeString()}</span>
</div>
${transId ? `<span class="ga4-diagnostic-badge success">ID: ${transId}</span>` : ''}
${value ? `<span class="ga4-diagnostic-badge">💰 ${value}</span>` : ''}
${itemName ? `<div style="font-size:9px; color:#aaa;">${itemName.substring(0, 40)}</div>` : ''}
${!isValid ? `<div class="ga4-diagnostic-error-list">❌ ${e.validation.errors[0]}</div>` : ''}
</div>`;
}).join('');
const logDiv = document.getElementById('ga4-diagnostic-log');
const allIssues = [...diagnosticData.errors, ...diagnosticData.warnings].reverse().slice(0, 10);
if (allIssues.length) {
logDiv.innerHTML = allIssues.map(issue => `
<div class="ga4-diagnostic-log-entry">
<span class="ga4-diagnostic-log-time">${new Date(issue.timestamp).toLocaleTimeString()}</span>
${issue.type === 'error' ? '🔴' : '⚠️'} [${issue.event || 'global'}] ${issue.message}
</div>
`).join('');
} else {
logDiv.innerHTML = '<div style="color:#8b8eae; text-align:center;">✅ Нет ошибок</div>';
}
const light = document.getElementById('ga4-diagnostic-status-light');
if (diagnosticData.errors.length > 0) {
light.style.background = '#f44336';
light.style.boxShadow = '0 0 5px #f44336';
} else if (diagnosticData.warnings.length > 0) {
light.style.background = '#ff9800';
light.style.boxShadow = '0 0 5px #ff9800';
} else {
light.style.background = '#4caf50';
light.style.boxShadow = '0 0 5px #4caf50';
}
}
window.showEventDetails = function(event) {
const modal = document.createElement('div');
modal.className = 'ga4-modal';
modal.innerHTML = `
<div class="ga4-modal-content">
<div style="display: flex; justify-content: space-between;">
<strong>🔍 ${event.name}</strong>
<button style="background:#c44536; border:none; color:white; padding:2px 8px; border-radius:4px; cursor:pointer;" onclick="this.closest('.ga4-modal').remove()">✕</button>
</div>
<div style="margin:12px 0;">🕐 ${new Date(event.timestamp).toLocaleString()}<br>🔗 ${event.url.split('/').pop() || '/'}<br>📡 ${event.source}</div>
${event.validation.errors.length ? `<div style="background:#4a2a2a; padding:8px; border-radius:6px; margin:12px 0;"><strong>❌ Ошибки:</strong><br>${event.validation.errors.join('<br>')}</div>` : ''}
${event.validation.warnings.length ? `<div style="background:#4a3a2a; padding:8px; border-radius:6px; margin:12px 0;"><strong>⚠️ Предупреждения:</strong><br>${event.validation.warnings.join('<br>')}</div>` : ''}
<div style="background:#1e1e2f; padding:12px; border-radius:8px;"><strong>📦 Данные:</strong><pre style="white-space:pre-wrap; font-size:10px;">${JSON.stringify(event.data, null, 2)}</pre></div>
</div>
`;
document.body.appendChild(modal);
modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
};
// ========== ПАССИВНЫЙ МОНИТОРИНГ (НЕ ПЕРЕХВАТЫВАЕТ) ==========
function setupPassiveMonitoring() {
// 1. Подписываемся на события GTM через addEventListener
if (window.dataLayer) {
// Сохраняем оригинальный метод, но НЕ переопределяем
// Просто следим через прокси без изменения функциональности
const originalPush = window.dataLayer.push;
window.dataLayer.push = function(...args) {
const result = originalPush.apply(this, args);
const event = args[0];
if (event && typeof event === 'object' && event.event) {
// Используем setTimeout, чтобы не блокировать выполнение
setTimeout(() => addEvent(event.event, event, 'datalayer'), 0);
}
return result;
};
console.log('✅ dataLayer мониторинг (пассивный)');
}
// 2. Для gtag используем перехват с сохранением функциональности
if (typeof window.gtag === 'function') {
const originalGtag = window.gtag;
window.gtag = function(...args) {
const result = originalGtag.apply(this, args);
if (args[0] === 'event') {
setTimeout(() => addEvent(args[1], args[2] || {}, 'direct_gtag'), 0);
}
return result;
};
console.log('✅ gtag мониторинг (пассивный)');
}
// 3. Поиск ID
const scripts = Array.from(document.querySelectorAll('script[src*="googletagmanager"]'));
const gtmScript = scripts.find(s => s.src && s.src.includes('gtm.js'));
if (gtmScript && !diagnosticData.gtmId) {
const match = gtmScript.src.match(/id=([^&]+)/);
if (match) { diagnosticData.gtmId = match[1]; saveData(); console.log(`✅ GTM ID: ${diagnosticData.gtmId}`); }
}
// 4. SPA мониторинг
let lastUrl = window.location.href;
const originalPushState = history.pushState;
history.pushState = function(...args) {
const result = originalPushState.apply(this, args);
setTimeout(checkUrlChange, 100);
return result;
};
const originalReplaceState = history.replaceState;
history.replaceState = function(...args) {
const result = originalReplaceState.apply(this, args);
setTimeout(checkUrlChange, 100);
return result;
};
window.addEventListener('popstate', () => setTimeout(checkUrlChange, 100));
function checkUrlChange() {
const newUrl = window.location.href;
if (newUrl !== lastUrl) {
lastUrl = newUrl;
setTimeout(() => {
if (!diagnosticData.pages.some(p => p.url === newUrl)) {
diagnosticData.pages.push({ url: newUrl, title: document.title, timestamp: new Date().toISOString() });
saveData();
console.log(`📄 SPA переход: ${newUrl.split('/').pop() || '/'}`);
}
}, 200);
}
}
console.log('✅ SPA мониторинг');
}
// ========== СКАЧИВАНИЕ ==========
function downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
}
function downloadJSON() {
downloadFile(JSON.stringify(diagnosticData, null, 2), `ga4_report_${new Date().toISOString().slice(0,19).replace(/:/g, '-')}.json`, 'application/json');
}
function downloadCSV() {
const headers = ['timestamp', 'event_name', 'source', 'url', 'valid', 'errors', 'warnings', 'transaction_id', 'value', 'currency'];
const rows = diagnosticData.events.map(e => [
e.timestamp, e.name, e.source, e.url, e.validation.isValid ? 'YES' : 'NO',
`"${e.validation.errors.join('; ')}"`, `"${e.validation.warnings.join('; ')}"`,
e.data.transaction_id || e.data.ecommerce?.transaction_id || '',
e.data.value || e.data.ecommerce?.value || '',
e.data.currency || e.data.ecommerce?.currency || ''
]);
downloadFile([headers, ...rows].map(r => r.join(',')).join('\n'), `ga4_events_${new Date().toISOString().slice(0,19).replace(/:/g, '-')}.csv`, 'text/csv;charset=utf-8;');
}
function downloadTXT() {
let content = `GA4 ДИАГНОСТИКА v5.1\n${'='.repeat(60)}\nДата: ${new Date().toISOString()}\nСтраниц: ${diagnosticData.pages.length}\nСобытий: ${diagnosticData.events.length}\nОшибок: ${diagnosticData.errors.length}\nПредупреждений: ${diagnosticData.warnings.length}\n\n${'='.repeat(60)}\nОШИБКИ:\n${diagnosticData.errors.map(e => `🔴 [${e.event}] ${e.message}`).join('\n')}\n\n${'='.repeat(60)}\nСОБЫТИЯ:\n${diagnosticData.events.map(e => `[${e.name}] ${e.validation.isValid ? '✓' : '✗'} ${e.validation.errors.join('; ')}`).join('\n')}`;
downloadFile(content, `ga4_report_${new Date().toISOString().slice(0,19).replace(/:/g, '-')}.txt`, 'text/plain;charset=utf-8;');
}
// ========== ПОДРОБНЫЙ HTML ОТЧЕТ ==========
function downloadHTML() {
const validEvents = diagnosticData.events.filter(e => e.validation.isValid);
const invalidEvents = diagnosticData.events.filter(e => !e.validation.isValid);
const purchaseEvents = diagnosticData.events.filter(e => e.name === 'purchase');
const hasDuplicate = purchaseEvents.length > 1;
const eventGroups = {};
diagnosticData.events.forEach(e => {
if (!eventGroups[e.name]) eventGroups[e.name] = [];
eventGroups[e.name].push(e);
});
const html = `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>GA4 Diagnostic Report</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
background: #f0f2f5;
padding: 40px 20px;
line-height: 1.5;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 8px 30px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: white;
padding: 30px 40px;
}
.header h1 {
font-size: 28px;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.header .meta {
opacity: 0.8;
font-size: 14px;
margin-top: 15px;
}
.status-card {
margin: 20px 40px;
padding: 20px;
border-radius: 16px;
font-weight: 500;
}
.status-card.success {
background: #e8f5e9;
border-left: 4px solid #4caf50;
color: #2e7d32;
}
.status-card.error {
background: #ffebee;
border-left: 4px solid #f44336;
color: #c62828;
}
.status-card.warning {
background: #fff3e0;
border-left: 4px solid #ff9800;
color: #ef6c00;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 20px;
padding: 0 40px 30px;
}
.stat-card {
background: #f8f9fa;
border-radius: 16px;
padding: 20px;
text-align: center;
transition: transform 0.2s;
}
.stat-card:hover { transform: translateY(-2px); }
.stat-value {
font-size: 36px;
font-weight: bold;
color: #1a1a2e;
}
.stat-label {
color: #666;
font-size: 13px;
margin-top: 8px;
}
.stat-value.valid { color: #4caf50; }
.stat-value.invalid { color: #f44336; }
.stat-value.warning { color: #ff9800; }
.section {
padding: 0 40px 30px;
border-bottom: 1px solid #e0e0e0;
}
.section-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 20px;
color: #1a1a2e;
display: flex;
align-items: center;
gap: 10px;
}
.funnel {
background: #f8f9fa;
border-radius: 12px;
padding: 20px;
}
.funnel-step {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid #e9ecef;
}
.funnel-step:last-child { border-bottom: none; }
.funnel-step.completed { color: #4caf50; }
.funnel-step.missing { color: #f44336; opacity: 0.7; }
.pages-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.page-item {
background: #f8f9fa;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
font-family: monospace;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e9ecef;
vertical-align: top;
}
th {
background: #f8f9fa;
font-weight: 600;
position: sticky;
top: 0;
}
.event-valid { background: #e8f5e9; }
.event-invalid { background: #ffebee; }
.event-warning { background: #fff3e0; }
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 20px;
font-size: 11px;
font-weight: 500;
}
.badge-valid { background: #4caf50; color: white; }
.badge-invalid { background: #f44336; color: white; }
.badge-warning { background: #ff9800; color: white; }
.items-list {
font-size: 11px;
background: #f8f9fa;
padding: 8px;
border-radius: 8px;
margin-top: 4px;
}
.item {
margin: 6px 0;
padding: 6px;
background: white;
border-radius: 6px;
}
.param-key { color: #ff6b6b; font-weight: 500; }
.param-value { color: #4caf50; }
.error-text { color: #f44336; font-size: 11px; margin-top: 4px; }
.warning-text { color: #ff9800; font-size: 11px; }
@media print {
body { background: white; padding: 0; }
.container { box-shadow: none; }
.status-card { break-inside: avoid; }
.section { break-inside: avoid; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔍 GA4 Диагностический отчет</h1>
<div class="meta">
Session ID: ${diagnosticData.sessionId}<br>
Начало сессии: ${new Date(diagnosticData.startTime).toLocaleString()}<br>
Сформирован: ${new Date().toLocaleString()}<br>
GTM ID: ${diagnosticData.gtmId || 'не найден'}
</div>
</div>
<div class="status-card ${hasDuplicate ? 'error' : (diagnosticData.errors.length > 0 ? 'error' : (diagnosticData.warnings.length > 0 ? 'warning' : 'success'))}">
<strong>${hasDuplicate ? '❌ ОБНАРУЖЕНО ДУБЛИРОВАНИЕ PURCHASE!' : (diagnosticData.errors.length > 0 ? '❌ НАЙДЕНЫ ОШИБКИ ВАЛИДАЦИИ' : (diagnosticData.warnings.length > 0 ? '⚠️ ЕСТЬ ПРЕДУПРЕЖДЕНИЯ' : '✅ ВСЕ ПРОВЕРКИ ПРОЙДЕНЫ'))}</strong>
${hasDuplicate ? `<br>Зафиксировано ${purchaseEvents.length} покупки(ок) — проверьте источники дублирования` : ''}
${diagnosticData.errors.length > 0 && !hasDuplicate ? `<br>Найдено ${diagnosticData.errors.length} критических ошибок в событиях` : ''}
</div>
<div class="stats-grid">
<div class="stat-card"><div class="stat-value">${diagnosticData.pages.length}</div><div class="stat-label">страниц</div></div>
<div class="stat-card"><div class="stat-value">${diagnosticData.events.length}</div><div class="stat-label">событий</div></div>
<div class="stat-card"><div class="stat-value valid">${validEvents.length}</div><div class="stat-label">валидных</div></div>
<div class="stat-card"><div class="stat-value invalid">${invalidEvents.length}</div><div class="stat-label">невалидных</div></div>
<div class="stat-card"><div class="stat-value warning">${diagnosticData.warnings.length}</div><div class="stat-label">предупреждений</div></div>
<div class="stat-card"><div class="stat-value invalid">${diagnosticData.errors.length}</div><div class="stat-label">ошибок</div></div>
</div>
<div class="section">
<div class="section-title">🎯 Воронка покупки</div>
<div class="funnel">
${[
{ step: 'view_item_list', desc: 'Просмотр списка товаров' },
{ step: 'select_item', desc: 'Выбор товара из списка' },
{ step: 'view_item', desc: 'Просмотр карточки товара' },
{ step: 'add_to_cart', desc: 'Добавление в корзину' },
{ step: 'begin_checkout', desc: 'Начало оформления' },
{ step: 'purchase', desc: 'Покупка' }
].map(({step, desc}) => `
<div class="funnel-step ${diagnosticData.funnel[step] ? 'completed' : 'missing'}">
${diagnosticData.funnel[step] ? '✅' : '⬜'} <strong>${step}</strong> — ${desc}
</div>
`).join('')}
</div>
</div>
<div class="section">
<div class="section-title">📄 Посещенные страницы</div>
<div class="pages-list">
${diagnosticData.pages.map(p => `
<div class="page-item" title="${p.url}">
${p.url.split('/').pop() || '/'} <span style="color:#999; font-size:10px;">${new Date(p.timestamp).toLocaleTimeString()}</span>
</div>
`).join('')}
</div>
</div>
<div class="section">
<div class="section-title">📡 Все события с параметрами</div>
<div style="overflow-x: auto;">
<table>
<thead>
<tr>
<th>Время</th>
<th>Событие</th>
<th>Статус</th>
<th>Transaction ID</th>
<th>Сумма</th>
<th>Параметры / Товары</th>
<th>Страница</th>
</tr>
</thead>
<tbody>
${diagnosticData.events.map(e => {
const eventData = e.data.ecommerce || e.data;
const items = eventData.items || [];
const transId = eventData.transaction_id || '';
const value = eventData.value || '';
const currency = eventData.currency || 'USD';
const rowClass = !e.validation.isValid ? 'event-invalid' : (e.validation.warnings.length > 0 ? 'event-warning' : 'event-valid');
const statusBadge = !e.validation.isValid ? 'invalid' : (e.validation.warnings.length > 0 ? 'warning' : 'valid');
let itemsHtml = '';
if (items.length > 0) {
itemsHtml = `<div class="items-list"><strong>📦 Товары (${items.length}):</strong>`;
items.forEach((item, idx) => {
itemsHtml += `
<div class="item">
<strong>#${idx+1}</strong><br>
${item.item_id ? `<span class="param-key">ID:</span> <span class="param-value">${item.item_id}</span><br>` : ''}
${item.item_name ? `<span class="param-key">Название:</span> <span class="param-value">${item.item_name.substring(0, 80)}</span><br>` : ''}
${item.item_brand ? `<span class="param-key">Бренд:</span> <span class="param-value">${item.item_brand}</span><br>` : ''}
${item.item_category ? `<span class="param-key">Категория:</span> <span class="param-value">${item.item_category}</span><br>` : ''}
${item.item_category2 ? `<span class="param-key">Категория 2:</span> <span class="param-value">${item.item_category2}</span><br>` : ''}
${item.price ? `<span class="param-key">Цена:</span> <span class="param-value">${item.price} ${currency}</span><br>` : ''}
${item.quantity ? `<span class="param-key">Кол-во:</span> <span class="param-value">${item.quantity}</span><br>` : ''}
${item.index !== undefined ? `<span class="param-key">Позиция:</span> <span class="param-value">${item.index}</span><br>` : ''}
</div>
`;
});
itemsHtml += `</div>`;
}
// Дополнительные параметры
let extraHtml = '';
if (eventData.creative_name) extraHtml += `<div><span class="param-key">🎨 Креатив:</span> ${eventData.creative_name}</div>`;
if (eventData.promotion_name) extraHtml += `<div><span class="param-key">🎁 Промо:</span> ${eventData.promotion_name}</div>`;
if (eventData.item_list_name && !items.some(i => i.item_list_name)) {
extraHtml += `<div><span class="param-key">📋 Список:</span> ${eventData.item_list_name}</div>`;
}
return `
<tr class="${rowClass}">
<td style="white-space: nowrap;">${new Date(e.timestamp).toLocaleString()}</td>
<td><strong>${e.name}</strong><br><small>${e.source}</small></td>
<td><span class="badge badge-${statusBadge}">${!e.validation.isValid ? 'ОШИБКА' : (e.validation.warnings.length > 0 ? 'ПРЕДУПРЕЖДЕНИЕ' : 'ВАЛИДНО')}</span></td>
<td>${transId || '-'}</td>
<td>${value ? `${currency} ${value}` : '-'}</td>
<td>${itemsHtml || extraHtml || '-'}</td>
<td>${e.url.split('/').pop() || '/'}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
</div>
<div class="section">
<div class="section-title">⚠️ Ошибки и предупреждения</div>
${diagnosticData.errors.length === 0 && diagnosticData.warnings.length === 0 ?
'<div style="padding: 20px; text-align: center; color: #4caf50;">✅ Нет ошибок и предупреждений</div>' :
`
${diagnosticData.errors.length > 0 ? `
<div style="margin-bottom: 20px;">
<strong style="color: #f44336;">🔴 ОШИБКИ (${diagnosticData.errors.length}):</strong>
<ul style="margin-top: 10px;">
${diagnosticData.errors.map(err => `<li style="color: #f44336;">[${err.event || 'global'}] ${err.message}</li>`).join('')}
</ul>
</div>
` : ''}
${diagnosticData.warnings.length > 0 ? `
<div>
<strong style="color: #ff9800;">🟠 ПРЕДУПРЕЖДЕНИЯ (${diagnosticData.warnings.length}):</strong>
<ul style="margin-top: 10px;">
${diagnosticData.warnings.map(warn => `<li style="color: #ff9800;">[${warn.event || 'global'}] ${warn.message}</li>`).join('')}
</ul>
</div>
` : ''}
`
}
</div>
<div class="section">
<div class="section-title">📊 Сводка по типам событий</div>
<table>
<thead><tr><th>Событие</th><th>Количество</th><th>Валидных</th><th>Невалидных</th><th>Примеры параметров</th></tr></thead>
<tbody>
${Object.entries(eventGroups).map(([name, events]) => {
const valid = events.filter(e => e.validation.isValid).length;
const invalid = events.length - valid;
const sample = events[0];
const eventData = sample.data.ecommerce || sample.data;
const items = eventData.items || [];
let example = '';
if (items.length > 0) {
example = `${items[0].item_name || items[0].item_id} — ${eventData.value || items[0].price} ${eventData.currency || 'USD'}`;
} else if (eventData.value) {
example = `value=${eventData.value} ${eventData.currency || 'USD'}`;
}
return `
<tr>
<td><strong>${name}</strong></td>
<td>${events.length}</td>
<td style="color:#4caf50;">${valid}</td>
<td style="color:#f44336;">${invalid}</td>
<td><small>${example || '-'}</small></td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
<div class="section">
<div class="section-title">🎯 Вывод по purchase</div>
${purchaseEvents.length === 0 ?
'<div>ℹ️ За время сессии не было зафиксировано ни одной покупки. Совершите тестовую покупку для проверки.</div>' :
(purchaseEvents.length === 1 ?
`<div style="background:#e8f5e9; padding:16px; border-radius:12px;">
<strong>✅ ЗАФИКСИРОВАНА 1 ПОКУПКА — дублирования НЕТ!</strong><br>
Transaction ID: ${purchaseEvents[0].data.transaction_id || purchaseEvents[0].data.ecommerce?.transaction_id || 'не указан'}<br>
Сумма: ${purchaseEvents[0].data.value || purchaseEvents[0].data.ecommerce?.value || 'не указана'}<br>
Время: ${new Date(purchaseEvents[0].timestamp).toLocaleString()}<br>
Источник: ${purchaseEvents[0].source}<br>
Страница: ${purchaseEvents[0].url.split('/').pop() || '/'}
</div>` :
`<div style="background:#ffebee; padding:16px; border-radius:12px;">
<strong>❌ ЗАФИКСИРОВАНО ${purchaseEvents.length} ПОКУПКИ(ОК) — ОБНАРУЖЕНО ДУБЛИРОВАНИЕ!</strong><br>
${purchaseEvents.map((p,i) => `${i+1}. ${new Date(p.timestamp).toLocaleTimeString()} — ID: ${p.data.transaction_id || p.data.ecommerce?.transaction_id || 'не указан'} — ${p.url.split('/').pop()}`).join('<br>')}
<br><br>
<strong>🔧 РЕКОМЕНДАЦИИ:</strong><br>
1. Проверьте триггер purchase в GTM — он должен срабатывать 1 раз<br>
2. Убедитесь, что нет двух источников (GTM + прямой gtag)<br>
3. Реализуйте защиту через sessionStorage/localStorage
</div>`)
}
</div>
</div>
</body>
</html>`;
downloadFile(html, `ga4_report_${new Date().toISOString().slice(0,19).replace(/:/g, '-')}.html`, 'text/html;charset=utf-8;');
}
function downloadValidationCSV() {
const headers = ['timestamp', 'event_name', 'type', 'message'];
const rows = [
...diagnosticData.errors.map(e => [e.timestamp, e.event, 'error', e.message]),
...diagnosticData.warnings.map(w => [w.timestamp, w.event, 'warning', w.message])
];
downloadFile([headers, ...rows].map(r => r.join(',')).join('\n'), `ga4_validation_${new Date().toISOString().slice(0,19).replace(/:/g, '-')}.csv`, 'text/csv;charset=utf-8;');
}
// ========== ПОДРОБНЫЙ ОТЧЕТ В КОНСОЛИ ==========
function showReport() {
const validEvents = diagnosticData.events.filter(e => e.validation.isValid);
const invalidEvents = diagnosticData.events.filter(e => !e.validation.isValid);
const purchaseEvents = diagnosticData.events.filter(e => e.name === 'purchase');
const hasDuplicate = purchaseEvents.length > 1;
console.clear();
console.log('\n' + '█'.repeat(80));
console.log('📊 GA4 ДИАГНОСТИЧЕСКИЙ ОТЧЕТ v5.1');
console.log('█'.repeat(80));
console.log(`🕐 Сессия начата: ${new Date(diagnosticData.startTime).toLocaleString()}`);
console.log(`🕐 Отчет сформирован: ${new Date().toLocaleString()}`);
console.log(`🆔 Session ID: ${diagnosticData.sessionId}`);
console.log(`🆔 GTM ID: ${diagnosticData.gtmId || 'не найден'}`);
console.log(`🆔 GA4 ID: ${diagnosticData.ga4Id || 'не найден'}`);
console.log('\n' + '─'.repeat(80));
console.log('📊 СТАТИСТИКА СЕССИИ:');
console.log('─'.repeat(80));
console.log(` 📄 Посещено страниц: ${diagnosticData.pages.length}`);
console.log(` 📡 Всего событий: ${diagnosticData.events.length}`);
console.log(` ✅ Валидных: ${validEvents.length}`);
console.log(` ❌ Невалидных: ${invalidEvents.length}`);
console.log(` 🛍️ Покупок: ${purchaseEvents.length}`);
console.log(` ⚠️ Предупреждений: ${diagnosticData.warnings.length}`);
console.log(` 🔴 Ошибок: ${diagnosticData.errors.length}`);
console.log('\n' + '─'.repeat(80));
console.log('🎯 ВОРОНКА ПОКУПКИ:');
console.log('─'.repeat(80));
const funnelSteps = [
{ step: 'view_item_list', desc: 'Просмотр списка товаров', required: false },
{ step: 'select_item', desc: 'Выбор товара из списка', required: false },
{ step: 'view_item', desc: 'Просмотр карточки товара', required: true },
{ step: 'add_to_cart', desc: 'Добавление в корзину', required: true },
{ step: 'begin_checkout', desc: 'Начало оформления', required: true },
{ step: 'purchase', desc: 'Покупка', required: true }
];
funnelSteps.forEach(({ step, desc, required }) => {
const completed = diagnosticData.funnel[step];
const icon = completed ? '✅' : (required ? '❌' : '⬜');
const color = completed ? '' : (required ? ' (ОБЯЗАТЕЛЬНО!)' : '');
console.log(` ${icon} ${step.padEnd(20)} → ${desc}${color}`);
});
console.log('\n' + '─'.repeat(80));
console.log('📄 ПОСЕЩЕННЫЕ СТРАНИЦЫ:');
console.log('─'.repeat(80));
diagnosticData.pages.forEach((page, i) => {
const shortUrl = page.url.replace(/^https?:\/\/[^\/]+/, '');
console.log(` ${(i+1).toString().padStart(2)}. ${shortUrl || '/'}`);
console.log(` 🕐 ${new Date(page.timestamp).toLocaleString()}`);
});
console.log('\n' + '─'.repeat(80));
console.log('📡 ВСЕ СОБЫТИЯ С ПОДРОБНЫМИ ПАРАМЕТРАМИ:');
console.log('─'.repeat(80));
diagnosticData.events.forEach((e, i) => {
const isValid = e.validation.isValid;
const statusIcon = !isValid ? '🔴' : (e.validation.warnings.length > 0 ? '🟠' : '✅');
console.log(`\n[${i+1}] ${statusIcon} ${e.name} | ${new Date(e.timestamp).toLocaleString()}`);
console.log(` 📡 Источник: ${e.source}`);
console.log(` 🔗 Страница: ${e.url.split('/').pop() || '/'}`);
// Основные параметры события
const eventData = e.data.ecommerce || e.data;
if (eventData.transaction_id) console.log(` 🆔 transaction_id: ${eventData.transaction_id}`);
if (eventData.value) console.log(` 💰 value: ${eventData.value} ${eventData.currency || 'USD'}`);
if (eventData.currency) console.log(` 💱 currency: ${eventData.currency}`);
if (eventData.item_list_name) console.log(` 📋 item_list_name: ${eventData.item_list_name}`);
// Параметры товаров
const items = eventData.items || [];
if (items.length > 0) {
console.log(` 📦 ТОВАРЫ (${items.length}):`);
items.forEach((item, idx) => {
console.log(` [${idx+1}] ─────────────────────────`);
if (item.item_id) console.log(` 🆔 item_id: ${item.item_id}`);
if (item.item_name) console.log(` 📝 item_name: ${item.item_name.substring(0, 80)}`);
if (item.item_brand) console.log(` 🏷️ item_brand: ${item.item_brand}`);
if (item.item_category) console.log(` 📁 item_category: ${item.item_category}`);
if (item.item_category2) console.log(` 📁 item_category2: ${item.item_category2}`);
if (item.item_category3) console.log(` 📁 item_category3: ${item.item_category3}`);
if (item.price) console.log(` 💰 price: ${item.price}`);
if (item.quantity) console.log(` 🔢 quantity: ${item.quantity}`);
if (item.index !== undefined) console.log(` 🔢 index: ${item.index}`);
if (item.item_list_name) console.log(` 📋 item_list_name: ${item.item_list_name}`);
});
// Проверка суммы
const sum = items.reduce((acc, item) => acc + (item.price || 0) * (item.quantity || 1), 0);
if (eventData.value && Math.abs(sum - eventData.value) > 0.01) {
console.log(` ⚠️ ВНИМАНИЕ: value (${eventData.value}) не совпадает с суммой товаров (${sum})`);
}
}
// Параметры промо
if (eventData.creative_name) console.log(` 🎨 creative_name: ${eventData.creative_name}`);
if (eventData.creative_slot) console.log(` 📍 creative_slot: ${eventData.creative_slot}`);
if (eventData.promotion_id) console.log(` 🎁 promotion_id: ${eventData.promotion_id}`);
if (eventData.promotion_name) console.log(` 🎁 promotion_name: ${eventData.promotion_name}`);
// Ошибки и предупреждения
if (e.validation.errors.length > 0) {
console.log(` ❌ ОШИБКИ (${e.validation.errors.length}):`);
e.validation.errors.forEach(err => console.log(` 🔴 ${err}`));
}
if (e.validation.warnings.length > 0) {
console.log(` ⚠️ ПРЕДУПРЕЖДЕНИЯ (${e.validation.warnings.length}):`);
e.validation.warnings.forEach(warn => console.log(` 🟠 ${warn}`));
}
});
console.log('\n' + '─'.repeat(80));
console.log('⚠️ ВСЕ ОШИБКИ И ПРЕДУПРЕЖДЕНИЯ:');
console.log('─'.repeat(80));
if (diagnosticData.errors.length === 0 && diagnosticData.warnings.length === 0) {
console.log(' ✅ Нет ошибок и предупреждений');
} else {
if (diagnosticData.errors.length > 0) {
console.log(` 🔴 ОШИБКИ (${diagnosticData.errors.length}):`);
diagnosticData.errors.forEach(err => {
console.log(` [${err.event || 'global'}] ${err.message}`);
});
}
if (diagnosticData.warnings.length > 0) {
console.log(` 🟠 ПРЕДУПРЕЖДЕНИЯ (${diagnosticData.warnings.length}):`);
diagnosticData.warnings.forEach(warn => {
console.log(` [${warn.event || 'global'}] ${warn.message}`);
});
}
}
console.log('\n' + '─'.repeat(80));
console.log('🎯 ВЫВОД ПО PURCHASE:');
console.log('─'.repeat(80));
if (purchaseEvents.length === 0) {
console.log(' ℹ️ За время сессии не было зафиксировано ни одной покупки.');
console.log(' 💡 Совершите тестовую покупку для проверки.');
} else if (purchaseEvents.length === 1) {
console.log(' ✅ ЗАФИКСИРОВАНА 1 ПОКУПКА — дублирования НЕТ!');
const p = purchaseEvents[0];
const transId = p.data.transaction_id || p.data.ecommerce?.transaction_id;
const value = p.data.value || p.data.ecommerce?.value;
console.log(` 🆔 Transaction ID: ${transId || 'не указан'}`);
console.log(` 💰 Сумма: ${value || 'не указана'}`);
console.log(` 🕐 Время: ${new Date(p.timestamp).toLocaleString()}`);
console.log(` 📡 Источник: ${p.source}`);
console.log(` 📄 Страница: ${p.url.split('/').pop() || '/'}`);
if (p.data.items && p.data.items.length > 0) {
console.log(` 📦 Товары:`);
p.data.items.forEach((item, idx) => {
console.log(` ${idx+1}. ${item.item_name || item.item_id} — ${item.price} x ${item.quantity} = ${item.price * item.quantity}`);
});
}
if (!p.validation.isValid) {
console.log(` ❌ ОШИБКИ ВАЛИДАЦИИ:`);
p.validation.errors.forEach(err => console.log(` 🔴 ${err}`));
}
} else {
console.log(` ❌ ЗАФИКСИРОВАНО ${purchaseEvents.length} ПОКУПКИ(ОК) — ОБНАРУЖЕНО ДУБЛИРОВАНИЕ!`);
console.log(`\n 📋 Детали дублей:`);
purchaseEvents.forEach((p, i) => {
const transId = p.data.transaction_id || p.data.ecommerce?.transaction_id;
const time = new Date(p.timestamp).toLocaleTimeString();
console.log(` ${i+1}. ${time} — ID: ${transId || 'не указан'} — ${p.url.split('/').pop()}`);
console.log(` Источник: ${p.source}`);
});
console.log(`\n 🔧 РЕКОМЕНДАЦИИ ПО ИСПРАВЛЕНИЮ:`);
console.log(` 1. Проверьте триггер purchase в GTM — он должен срабатывать 1 раз`);
console.log(` 2. Убедитесь, что нет двух источников (GTM + прямой gtag)`);
console.log(` 3. Реализуйте защиту через sessionStorage/localStorage`);
console.log(` 4. Проверьте, не срабатывает ли событие при обновлении страницы "Спасибо"`);
}
console.log('\n' + '─'.repeat(80));
console.log('📊 СВОДКА ПО ТИПАМ СОБЫТИЙ:');
console.log('─'.repeat(80));
const eventGroups = {};
diagnosticData.events.forEach(e => {
if (!eventGroups[e.name]) eventGroups[e.name] = [];
eventGroups[e.name].push(e);
});
Object.keys(eventGroups).sort().forEach(eventName => {
const events = eventGroups[eventName];
const validCount = events.filter(e => e.validation.isValid).length;
const invalidCount = events.length - validCount;
const statusIcon = invalidCount > 0 ? '❌' : (events.some(e => e.validation.warnings.length > 0) ? '🟠' : '✅');
console.log(` ${statusIcon} ${eventName.padEnd(20)} → ${events.length} раз(а) (валидных: ${validCount}, невалидных: ${invalidCount})`);
// Показываем пример параметров для первых 2 событий
events.slice(0, 2).forEach(e => {
const eventData = e.data.ecommerce || e.data;
const items = eventData.items || [];
if (items.length > 0) {
const firstItem = items[0];
console.log(` 📦 Пример: ${firstItem.item_name || firstItem.item_id} — ${eventData.value || firstItem.price} ${eventData.currency || 'USD'}`);
} else if (eventData.value) {
console.log(` 💰 Пример: value=${eventData.value} ${eventData.currency || 'USD'}`);
}
});
});
console.log('\n' + '█'.repeat(80));
console.log('💾 Отчет сохранен в window.__ga4FullReport');
console.log('📋 Чтобы скопировать JSON: copy(JSON.stringify(window.__ga4FullReport, null, 2))');
console.log('📥 Доступные форматы скачивания: JSON, CSV, TXT, HTML');
console.log('█'.repeat(80) + '\n');
window.__ga4FullReport = {
session: diagnosticData,
summary: {
totalPages: diagnosticData.pages.length,
totalEvents: diagnosticData.events.length,
validEvents: validEvents.length,
invalidEvents: invalidEvents.length,
purchaseCount: purchaseEvents.length,
hasDuplicate: hasDuplicate,
errorCount: diagnosticData.errors.length,
warningCount: diagnosticData.warnings.length,
funnel: diagnosticData.funnel
}
};
return window.__ga4FullReport;
}
function setupUI() {
let collapsed = false;
const header = document.getElementById('ga4-diagnostic-header');
const content = document.getElementById('ga4-diagnostic-content');
header.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON') return;
collapsed = !collapsed;
content.style.display = collapsed ? 'none' : 'block';
document.getElementById('ga4-diagnostic-collapse-icon').textContent = collapsed ? '▲' : '▼';
});
document.getElementById('ga4-btn-report').addEventListener('click', showReport);
document.getElementById('ga4-download-json').addEventListener('click', downloadJSON);
document.getElementById('ga4-download-csv').addEventListener('click', downloadCSV);
document.getElementById('ga4-download-txt').addEventListener('click', downloadTXT);
document.getElementById('ga4-download-html').addEventListener('click', downloadHTML);
document.getElementById('ga4-download-validation').addEventListener('click', downloadValidationCSV);
document.getElementById('ga4-btn-reset').addEventListener('click', () => {
if (confirm('Сбросить все данные?')) { localStorage.removeItem(STORAGE_KEY); location.reload(); }
});
}
// ========== ЗАПУСК ==========
setupPassiveMonitoring();
setupUI();
updateFunnel();
updateUI();
console.log('\n' + '='.repeat(60));
console.log('🔍 GA4 ДИАГНОСТИКА v5.1 ЗАПУЩЕНА');
console.log('✅ Режим: ТОЛЬКО НАБЛЮДЕНИЕ (не мешает отправке)');
console.log('📌 Панель в правом нижнем углу');
console.log('='.repeat(60) + '\n');
})();Install requires the InjectJS Chrome extension. Scripts run only on sites matching the pattern above. Review code before installing any community script.