/** * Telegraphyx Landing Connector * Клиентский JavaScript для встраивания на лендинги * Автоматически отслеживает визиты и управляет редиректами */ ;(function () { 'use strict' // Глобальный объект для Landing Connector window.TgphxLC = { // Константы UTM_KEYS: ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'], // Состояние inited: false, hasYM: false, visitEpoch: 0, // Параметры из конфигурации targetUrl: '', tgChannelID: '', tgBotName: '', botID: '', landing: '', start: '', vkPixelID: '', yaCounterID: 0, buttonClassPattern: '', postButtonClassPattern: '', redirectToChannel: false, withoutRedirectToChannel: false, // Собранные данные yaClientID: null, tgphxID: null, yclid: null, isad: null, sid: null, rb_clickid: null, utmParams: {}, clientId: null, /** * Инициализация с конфигурацией */ init: function (config) { // Сохраняем время визита var now = new Date() var utcTimestamp = Date.UTC( now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds(), now.getUTCMilliseconds() ) this.visitEpoch = Math.floor(utcTimestamp / 1000) // Применяем конфигурацию this.targetUrl = config.targetUrl this.tgChannelID = config.tgChannelID || '' this.tgBotName = config.tgBotName || '' this.botID = config.botID || '' this.landing = config.landing || '' this.start = config.start || '' this.vkPixelID = config.vkPixelID || '' this.yaCounterID = config.yaCounterID this.buttonClassPattern = config.buttonClassPattern this.postButtonClassPattern = config.postButtonClassPattern this.redirectToChannel = config.redirectToChannel this.withoutRedirectToChannel = config.withoutRedirectToChannel // Инициализация Яндекс.Метрики this.hasYM = typeof ym === 'function' if (this.hasYM && this.yaCounterID) { try { ym(this.yaCounterID, 'init', { clickmap: false, trackLinks: true, accurateTrackBounce: true, webvisor: false, triggerEvent: true, }) } catch (e) { console.error('Failed to init Yandex Metrika:', e) } } // Установка слушателей событий var self = this // DOM загружен if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { self.onDOMContentLoaded() }) } else { self.onDOMContentLoaded() } // Яндекс.Метрика инициализирована if (this.hasYM) { window.addEventListener('yacounter' + this.yaCounterID + 'inited', function () { self.inited = true self.onYaCounterInited() }) } else { // Если нет Метрики, сразу запускаем setTimeout(function () { self.onYaCounterInited() }, 100) } }, /** * Обработчик загрузки DOM */ onDOMContentLoaded: function () { var self = this // Получаем tgphxID из cookies this.tgphxID = this.getCookie('tgphxID') // Получаем параметры из URL this.yclid = this.getURLParam('yclid') // UTM метки this.utmParams = this.getUTMParams() // Cookies Яндекс.Метрики this.isad = this.getCookie('_ym_isad') this.sid = this.getCookie('yabs-sid') this.yaClientID = this.getCookie('_ym_uid') || this.getLocalStorage('_ym_uid') // VK Pixel if (this.vkPixelID) { this.rb_clickid = this.getURLParam('rb_clickid') } // Настройка кнопок - всегда перехватываем клики для корректной работы конверсий this.setupButtons() // Если Метрика еще не готова, запускаем без нее if (!this.inited) { this.onYaCounterInited() } }, /** * Обработчик инициализации Яндекс.Метрики */ onYaCounterInited: function () { var self = this // Пытаемся получить yaClientID от Метрики if (!this.yaClientID && this.hasYM && this.yaCounterID) { try { ym(this.yaCounterID, 'getClientID', function (clientId) { if (clientId) { self.yaClientID = String(clientId).replace(/^"|"$/g, '') } self.startVisitAndRedirect() }) return } catch (e) { console.error('Failed to get Yandex client ID:', e) } } // Запускаем без yaClientID this.startVisitAndRedirect() }, /** * Отправка визита и редирект */ startVisitAndRedirect: function () { var self = this // Устанавливаем fallback ссылку из кнопки если ещё не установлена if (!this._fallbackHref) { var mainButton = document.getElementById('button-link') if (mainButton) { this._fallbackHref = mainButton.getAttribute('href') } } // Определяем тип визита var visitType = this.tgBotName ? 'visit_bot' : 'visit' // Отправляем визит this.sendClientVisit(visitType, function () { // Ждем немного для надежности отправки setTimeout(function () { // Выполняем редирект если настроен if (self.redirectToChannel !== false && !self.withoutRedirectToChannel) { if (self.redirectToChannel === 'bot' || self.tgBotName) { self.doRedirectToBot() } else { self.doRedirectToChannel() } } }, 500) }) }, /** * Отправка данных о визите */ sendClientVisit: function (path, callback) { var self = this var params = { visitEpoch: String(this.visitEpoch), yaCounterID: String(this.yaCounterID), } // Добавляем параметры если есть if (this.tgphxID) params.tgphxID = this.tgphxID if (this.tgChannelID) params.tgChannelID = this.tgChannelID if (this.tgBotName) params.tgBotName = this.tgBotName if (this.vkPixelID) params.vkPixelID = this.vkPixelID if (this.rb_clickid) params.rb_clickid = this.rb_clickid if (this.yaClientID) params.yaClientID = this.yaClientID if (this.yclid) params.yclid = this.yclid if (this.isad) params.isad = this.isad if (this.sid) params.sid = this.sid // UTM метки for (var key in this.utmParams) { params[key] = this.utmParams[key] } // Формируем URL var url = this.targetUrl + '/api/landing/' + path var queryString = this.buildQueryString(params) url += '?' + queryString // Отправляем запрос var xhr = new XMLHttpRequest() xhr.open('POST', url, true) xhr.setRequestHeader('Content-Type', 'application/json') xhr.onload = function () { // Проверяем статус ответа - если ошибка, делаем fallback if (xhr.status >= 400) { console.error('Visit request failed with status:', xhr.status) if (self._fallbackHref && self._fallbackHref !== '#') { if (self._fallbackTimeout) { clearTimeout(self._fallbackTimeout) self._fallbackTimeout = null } window.location.href = self._fallbackHref return } if (callback) callback() return } try { var response = JSON.parse(xhr.responseText) if (response.clientId) { self.clientId = response.clientId } if (response.tgphxID) { self.tgphxID = response.tgphxID // Сохраняем в cookie на 30 дней self.setCookie('tgphxID', response.tgphxID, 30) } } catch (e) { console.error('Failed to parse visit response:', e) } if (callback) callback() } xhr.onerror = function () { console.error('Failed to send visit') // При ошибке сразу делаем fallback по прямой ссылке если есть if (self._fallbackHref && self._fallbackHref !== '#') { // Отменяем таймаут if (self._fallbackTimeout) { clearTimeout(self._fallbackTimeout) self._fallbackTimeout = null } window.location.href = self._fallbackHref return } if (callback) callback() } xhr.send() }, /** * Редирект на канал */ doRedirectToChannel: function () { if (!this.tgChannelID || !this.clientId) return // Отменяем fallback таймаут если был установлен if (this._fallbackTimeout) { clearTimeout(this._fallbackTimeout) this._fallbackTimeout = null } var redirectToPost = this.redirectToChannel if (redirectToPost === true) redirectToPost = '' var url = this.targetUrl + '/api/landing/redirect' url += '?tgChannelID=' + encodeURIComponent(this.tgChannelID) url += '&clientId=' + encodeURIComponent(this.clientId) if (redirectToPost && redirectToPost !== true) { url += '&redirectToPost=' + encodeURIComponent(redirectToPost) } // Отправляем онлайн конверсию в Яндекс.Метрику с callback // Используем callback для редиректа после отправки + fallback таймаут var redirected = false var doRedirect = function () { if (redirected) return redirected = true window.location.href = url } // Проверяем доступность ym() прямо перед вызовом (может быть заблокирован AdBlock) var ymAvailable = typeof ym === 'function' if (ymAvailable && this.yaCounterID) { try { // Callback вызовется после успешной отправки данных в Метрику ym(this.yaCounterID, 'reachGoal', 'toChannel', {}, doRedirect) // Fallback таймаут на случай если callback не сработает (ошибка сети, блокировщик и т.д.) setTimeout(doRedirect, 500) } catch (e) { // Ошибка при вызове ym() - сразу делаем редирект doRedirect() } } else { // Метрика недоступна (заблокирована) - сразу делаем редирект doRedirect() } }, /** * Редирект на бота */ doRedirectToBot: function () { if (!this.clientId) return // Отменяем fallback таймаут если был установлен if (this._fallbackTimeout) { clearTimeout(this._fallbackTimeout) this._fallbackTimeout = null } var url = this.targetUrl + '/api/bot/redirect' url += '?clientId=' + encodeURIComponent(this.clientId) if (this.tgBotName) url += '&tgBotName=' + encodeURIComponent(this.tgBotName) if (this.tgChannelID) url += '&tgChannelID=' + encodeURIComponent(this.tgChannelID) if (this.start) url += '&start=' + encodeURIComponent(this.start) if (this.landing) url += '&landing=' + encodeURIComponent(this.landing) if (this.botID) url += '&botID=' + encodeURIComponent(this.botID) // Отправляем онлайн конверсию в Яндекс.Метрику с callback // Используем callback для редиректа после отправки + fallback таймаут var redirected = false var doRedirect = function () { if (redirected) return redirected = true window.location.href = url } // Проверяем доступность ym() прямо перед вызовом (может быть заблокирован AdBlock) var ymAvailable = typeof ym === 'function' if (ymAvailable && this.yaCounterID) { try { // Callback вызовется после успешной отправки данных в Метрику ym(this.yaCounterID, 'reachGoal', 'toBot', {}, doRedirect) // Fallback таймаут на случай если callback не сработает (ошибка сети, блокировщик и т.д.) setTimeout(doRedirect, 500) } catch (e) { // Ошибка при вызове ym() - сразу делаем редирект doRedirect() } } else { // Метрика недоступна (заблокирована) - сразу делаем редирект doRedirect() } }, /** * Настройка кнопок для редиректа * Перехватывает клики на кнопках и направляет через API для корректной работы конверсий */ setupButtons: function () { var self = this var buttons = [] var addedElements = [] // Ищем основную кнопку по ID var mainButton = document.getElementById('button-link') if (mainButton) { buttons.push(mainButton) addedElements.push(mainButton) } // Также ищем по паттерну класса (для обратной совместимости) if (this.buttonClassPattern) { var patternButtons = document.querySelectorAll('[class*="' + this.buttonClassPattern + '"]') for (var i = 0; i < patternButtons.length; i++) { // Избегаем дубликатов if (addedElements.indexOf(patternButtons[i]) === -1) { buttons.push(patternButtons[i]) addedElements.push(patternButtons[i]) } } } for (var i = 0; i < buttons.length; i++) { var button = buttons[i] button.style.cursor = 'pointer' button.addEventListener('click', function (e) { e.preventDefault() // Сохраняем fallback ссылку из href для случая если API заблокирован var fallbackHref = e.currentTarget.getAttribute('href') self._fallbackHref = fallbackHref // Проверяем класс для определения номера поста var postMatch = e.target.className ? e.target.className.match(new RegExp(self.postButtonClassPattern)) : null var redirectToPost = postMatch ? postMatch[1] : true // Если есть clientId - сразу редиректим if (self.clientId) { self.redirectToChannel = redirectToPost if (self.tgBotName) { self.doRedirectToBot() } else { self.doRedirectToChannel() } } else { // Устанавливаем fallback таймаут - если API не ответит за 3 секунды, переходим по прямой ссылке var fallbackTimeout = null if (fallbackHref && fallbackHref !== '#') { fallbackTimeout = setTimeout(function () { window.location.href = fallbackHref }, 3000) } // Сохраняем таймаут чтобы отменить его при успешном редиректе self._fallbackTimeout = fallbackTimeout // Отправляем визит и потом редиректим self.redirectToChannel = redirectToPost self.startVisitAndRedirect() } }) } }, // === Утилиты === getCookie: function (name) { var cookies = document.cookie.split('; ') for (var i = 0; i < cookies.length; i++) { var cookie = cookies[i] if (cookie.indexOf(name + '=') === 0) { return cookie.substring(name.length + 1) } } return null }, setCookie: function (name, value, days) { var expires = '' if (days) { var date = new Date() date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000) expires = '; expires=' + date.toUTCString() } document.cookie = name + '=' + value + expires + '; path=/' }, getLocalStorage: function (key) { try { if (window.localStorage) { return window.localStorage.getItem(key) } } catch (e) {} return null }, getURLParam: function (param) { var match = RegExp('[?&]' + param + '=([^&]*)').exec(window.location.search) return match ? decodeURIComponent(match[1].replace(/\+/g, ' ')) : null }, getUTMParams: function () { var params = {} for (var i = 0; i < this.UTM_KEYS.length; i++) { var key = this.UTM_KEYS[i] var value = this.getURLParam(key) if (value) { params[key] = value } } return params }, buildQueryString: function (params) { var parts = [] for (var key in params) { if (params.hasOwnProperty(key) && params[key] !== null && params[key] !== undefined) { parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])) } } return parts.join('&') }, } TgphxLC.init(JSON.parse("{\"targetUrl\":\"https://tgryx.ru\",\"tgChannelID\":\"-1002310964816\",\"tgBotName\":\"\",\"botID\":\"\",\"vkPixelID\":\"\",\"start\":\"\",\"landing\":\"\",\"yaCounterID\":103763052,\"buttonClassPattern\":\"header__button\",\"postButtonClassPattern\":\"telegraphyx-to-channel-post-(\\\\d+)\",\"redirectToChannel\":false,\"withoutRedirectToChannel\":true}"));})();