(() => { 'use strict'; // Set once before uploading this stable file to GetCourse. It must belong // to infrastructure independent from both Foxplan and Naverivo. const FAILURE_REPORT_URL = ''; const LOAD_TIMEOUT_MS = 4000; // Stable bootstrap installed in GetCourse once. URL exclusions and the // Foxplan/Naverivo fallback stay here; product behavior stays in Pixel. const loader = document.currentScript; if (!(loader instanceof HTMLScriptElement)) { return; } const pixelKey = loader.dataset.pixel; if (!pixelKey || !/^[0-9a-f-]{36}$/i.test(pixelKey)) { return; } const normalizePath = value => value.replace(/\/+$/, '') || '/'; const currentPage = new URL(window.location.href); let excludedUrls = []; try { excludedUrls = JSON.parse(loader.dataset.exclude || '[]'); } catch { excludedUrls = []; } const excluded = excludedUrls.some(value => { try { const page = new URL(value, currentPage.origin); return page.hostname.toLowerCase() === currentPage.hostname.toLowerCase() && normalizePath(page.pathname) === normalizePath(currentPage.pathname) && [...page.searchParams].every(([name, expected]) => currentPage.searchParams.get(name) === expected ); } catch { return false; } }); if (excluded || window.__foxplanLoaderStarted) { return; } window.__foxplanLoaderStarted = true; const load = url => new Promise(resolve => { const started = performance.now(); const script = document.createElement('script'); let finished = false; const finish = error => { if (finished) { return; } finished = true; window.clearTimeout(timeout); script.onload = null; script.onerror = null; if (error) { script.remove(); } resolve({ error, duration_ms: Math.round(performance.now() - started), }); }; const timeout = window.setTimeout(() => finish('timeout'), LOAD_TIMEOUT_MS); script.src = url; script.async = true; script.onload = () => finish(null); script.onerror = () => finish('network_or_block'); document.head.appendChild(script); }); const showReportButton = tests => { if (currentPage.searchParams.get('foxplan_debug') !== '1') { return; } const button = document.createElement('button'); let publicIp = null; let ipLookup = 'pending'; const report = () => JSON.stringify({ generated_at: new Date().toISOString(), page: currentPage.origin + currentPage.pathname, public_ip: publicIp, ip_lookup: ipLookup, online: navigator.onLine, user_agent: navigator.userAgent, language: navigator.language || null, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || null, tests, }, null, 2); button.type = 'button'; button.disabled = true; button.textContent = 'Подготовка отчёта Foxplan…'; button.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:2147483647;padding:10px 14px;border:0;border-radius:8px;background:#f59e0b;color:#111827;font:bold 14px Arial;cursor:pointer'; button.onclick = () => { const contents = report(); const copy = navigator.clipboard?.writeText(contents); if (!copy) { window.prompt('Скопируйте отчёт', contents); return; } copy .then(() => { button.textContent = 'Отчёт скопирован'; }) .catch(() => window.prompt('Скопируйте отчёт', contents)); }; const mount = () => document.body.appendChild(button); if (document.body) { mount(); } else { document.addEventListener('DOMContentLoaded', mount, {once: true}); } const controller = new AbortController(); const timeout = window.setTimeout(() => controller.abort(), 3000); fetch('https://api64.ipify.org?format=json', { cache: 'no-store', credentials: 'omit', referrerPolicy: 'no-referrer', signal: controller.signal, }) .then(response => response.ok ? response.json() : Promise.reject(new Error('http'))) .then(result => { const value = String(result.ip || '').trim(); if (!/^[0-9a-f:.]{3,45}$/i.test(value)) { throw new Error('invalid_ip'); } publicIp = value; ipLookup = 'ok'; }) .catch(error => { ipLookup = error.name === 'AbortError' ? 'timeout' : 'unavailable'; }) .finally(() => { window.clearTimeout(timeout); button.disabled = false; button.textContent = 'Скопировать отчёт Foxplan'; }); }; const reportTotalFailure = tests => { if (!/^https:\/\//i.test(FAILURE_REPORT_URL)) { return; } const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection || {}; const userId = Number(window.accountUserId); const eventId = window.crypto?.randomUUID?.() || 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, character => { const random = Math.random() * 16 | 0; return (character === 'x' ? random : (random & 3 | 8)).toString(16); }); const body = JSON.stringify({ event_id: eventId, site_key: pixelKey, observed_at: new Date().toISOString(), result: 'failed', reason: 'both_endpoints_unavailable', external_user_id: Number.isSafeInteger(userId) && userId > 0 ? userId : null, origin_host: currentPage.hostname, page_path: currentPage.pathname, online: navigator.onLine, language: navigator.language || null, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || null, platform: navigator.userAgentData?.platform || navigator.platform || null, connection: { type: connection.type || null, effective_type: connection.effectiveType || null, downlink: connection.downlink ?? null, rtt: connection.rtt ?? null, save_data: Boolean(connection.saveData), }, tests: [ {name: 'foxplan_direct', ok: false, duration_ms: tests.direct.duration_ms, error: tests.direct.error}, {name: 'naverivo_proxy', ok: false, duration_ms: tests.fallback.duration_ms, error: tests.fallback.error}, ], script_version: '3.0.0', }); fetch(FAILURE_REPORT_URL, { method: 'POST', mode: 'no-cors', cache: 'no-store', credentials: 'omit', keepalive: true, headers: {'Content-Type': 'text/plain;charset=UTF-8'}, body, }).catch(() => {}); }; const run = async () => { window.__foxplanDelivery = {mode: 'direct'}; const direct = await load(`https://foxplan.online/assets/runtime/${pixelKey}.js`); if (!direct.error) { return; } window.__foxplanDelivery = { mode: 'fallback', directError: direct.error, directDuration: direct.duration_ms, }; const fallback = await load(`https://naverivo.com/assets/runtime/${pixelKey}.js`); if (fallback.error) { reportTotalFailure({direct, fallback}); showReportButton({direct, fallback}); } }; void run(); })();