PageSourceSearch

https://qform.io/assets/js/user-segment-array.js

js qform.io collected 2026-10-03 19:39:16 UTC 18,362 bytes, 511 lines download raw bytes

1// Определяем текущий хост
2const currentHost = window.location.host;
3
4// Функция для получения домена второго уровня
5function getSecondLevelDomain() {
6    const hostname = window.location.hostname;
7
8    // Обработка локальных хостов и IP-адресов
9    if (hostname === 'localhost' || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
10        return hostname;
11    }
12
13    const parts = hostname.split('.');
14
15    // Если это домен второго уровня (например, example.com)
16    if (parts.length === 2) {
17        return hostname;
18    }
19
20    // Для доменов третьего уровня и выше (например, sub.example.com)
21    // Берем последние 2 части
22    if (parts.length > 2) {
23        return parts.slice(-2).join('.');
24    }
25
26    return hostname;
27}
28
29// Имя кук (обновленные названия)
30const COOKIE_REFER = 'qform_segment_refer';
31const COOKIE_ENTRY_POINT = 'qform_segment_entry_point';
32const COOKIE_SEGMENT = 'qform_segment_type';
33
34// Получаем домен для кук
35const COOKIE_DOMAIN = getSecondLevelDomain();
36
37// URL для Viki API
38const VIKI_API_URL = 'https://viki.seotask.ru/qform/segment';
39
40// Утилиты для работы с куками
41const CookieUtils = {
42    // Получить куку по имени
43    get(name) {
44        const matches = document.cookie.match(new RegExp(
45            "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
46        ));
47        return matches ? decodeURIComponent(matches[1]) : undefined;
48    },
49
50    // Установить куку
51    set(name, value, options = {}) {
52        const defaultOptions = {
53            path: '/',
54            maxAge: 30 * 24 * 60 * 60, // 30 дней
55            sameSite: 'Lax',
56            domain: COOKIE_DOMAIN
57        };
58
59        const opts = { ...defaultOptions, ...options };
60
61        let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
62
63        if (opts.maxAge) {
64            cookieString += `; max-age=${opts.maxAge}`;
65        }
66
67        if (opts.path) {
68            cookieString += `; path=${opts.path}`;
69        }
70
71        // Добавляем домен только если это не localhost или IP-адрес
72        if (opts.domain && opts.domain !== 'localhost' && !opts.domain.startsWith('127.0.0.1') && !opts.domain.startsWith('192.168.')) {
73            cookieString += `; domain=.${opts.domain}`;
74        }
75
76        if (opts.secure) {
77            cookieString += '; secure';
78        }
79
80        if (opts.sameSite) {
81            cookieString += `; samesite=${opts.sameSite}`;
82        }
83
84        document.cookie = cookieString;
85    },
86
87    // Удалить куку
88    remove(name) {
89        this.set(name, '', { maxAge: -1 });
90    },
91
92    // Проверить, существует ли кука
93    exists(name) {
94        return this.get(name) !== undefined;
95    }
96};
97
98// Проверяем, является ли рефер внешним
99function isExternalReferrer(referrer) {
100    if (!referrer || referrer === '') {
101        return false;
102    }
103
104    try {
105        const referrerHost = new URL(referrer).host;
106        return referrerHost !== currentHost;
107    } catch (e) {
108        return false;
109    }
110}
111
112// Нормализовать массив сегментов: убрать пустые значения и дубли,
113// сохранив исходный порядок Viki.
114function normalizeSegments(segments) {
115    const sourceSegments = Array.isArray(segments)
116        ? segments
117        : (typeof segments === 'string' ? [segments] : []);
118
119    const uniqueSegments = new Set();
120
121    return sourceSegments.reduce((result, segment) => {
122        if (typeof segment !== 'string') {
123            return result;
124        }
125
126        const normalizedSegment = segment.trim();
127        if (!normalizedSegment || uniqueSegments.has(normalizedSegment)) {
128            return result;
129        }
130
131        uniqueSegments.add(normalizedSegment);
132        result.push(normalizedSegment);
133
134        return result;
135    }, []);
136}
137
138// Получить массив сегментов из куки. Старое одиночное значение
139// поддерживается только для бесшовной миграции на JSON-массив.
140function getSegmentsFromCookie() {
141    const value = CookieUtils.get(COOKIE_SEGMENT);
142    if (!value) {
143        return [];
144    }
145
146    try {
147        return normalizeSegments(JSON.parse(value));
148    } catch (e) {
149        return normalizeSegments([value]);
150    }
151}
152
153function saveSegments(segments) {
154    const normalizedSegments = normalizeSegments(segments);
155    CookieUtils.set(COOKIE_SEGMENT, JSON.stringify(normalizedSegments));
156    updateRegLinksWithSegments(normalizedSegments);
157    return normalizedSegments;
158}
159
160// Перед добавлением актуальных segments удалить прежние segment/segments.
161function updateRegLinksWithSegments(segments = getSegmentsFromCookie()) {
162    const normalizedSegments = normalizeSegments(segments);
163
164    const links = document.querySelectorAll('a.reglink');
165    if (!links || links.length === 0) return;
166
167    links.forEach((link) => {
168        const href = link.getAttribute('href');
169        if (!href) return;
170
171        try {
172            const url = new URL(href, window.location.origin);
173            url.searchParams.delete('segment');
174            url.searchParams.delete('segments');
175
176            normalizedSegments.forEach((segment) => {
177                url.searchParams.append('segment', segment);
178            });
179
180            // если ссылка относительная — сохраняем относительность
181            const isRelative = !/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(href);
182            link.setAttribute('href', isRelative ? (url.pathname + url.search + url.hash) : url.toString());
183        } catch (e) {
184            // некорректный href — пропускаем
185        }
186    });
187}
188
189// Поддерживаем старый segment и новый segments. Значение каждого поля
190// может быть строкой или массивом; при наличии segments оно приоритетно.
191function getSegmentsFromVikiResponse(data) {
192    if (!data || typeof data !== 'object') {
193        return [];
194    }
195
196    const sourceSegments = Object.prototype.hasOwnProperty.call(data, 'segments')
197        ? data.segments
198        : data.segment;
199
200    return normalizeSegments(sourceSegments);
201}
202
203// Все способы получения ответа Viki используют один и тот же обработчик.
204function handleVikiResponse(data, source = 'Viki API') {
205    const segments = saveSegments(getSegmentsFromVikiResponse(data));
206
207    console.log('Segments saved to cookie via ' + source + ':', segments);
208
209    if (data && typeof data === 'object' && !Array.isArray(data)) {
210        return { ...data, segments };
211    }
212
213    return { segments };
214}
215
216// Обработка реферов
217function processReferrer() {
218    const currentReferrer = document.referrer;
219
220    if (!isExternalReferrer(currentReferrer)) {
221        return;
222    }
223
224    let referrers = [];
225
226    // Получаем существующие реферы из куки
227    const existingReferrersCookie = CookieUtils.get(COOKIE_REFER);
228    if (existingReferrersCookie) {
229        try {
230            referrers = JSON.parse(existingReferrersCookie);
231            // Если это массив, сохраняем только первый элемент и добавляем новый
232            if (Array.isArray(referrers) && referrers.length > 0) {
233                // Сохраняем первый рефер
234                const firstReferrer = referrers[0];
235                referrers = [firstReferrer, currentReferrer];
236            } else {
237                // Если что-то пошло не так, начинаем заново
238                referrers = [currentReferrer];
239            }
240        } catch (e) {
241            referrers = [currentReferrer];
242        }
243    } else {
244        // Первый заход
245        referrers = [currentReferrer];
246    }
247
248    // Сохраняем в куку
249    CookieUtils.set(COOKIE_REFER, JSON.stringify(referrers));
250}
251
252// Обработка точек входа
253function processEntryPoint() {
254    const currentReferrer = document.referrer;
255
256    // Проверяем, есть ли переход с внешнего источника
257    if (!isExternalReferrer(currentReferrer)) {
258        return false;
259    }
260
261    // Получаем текущий URL без параметров и хоста (только путь)
262    const currentUrl = window.location.pathname; // Например: "/forms"
263
264    let entryPoints = [];
265
266    // Получаем существующие точки входа из куки
267    const existingEntryPointsCookie = CookieUtils.get(COOKIE_ENTRY_POINT);
268    if (existingEntryPointsCookie) {
269        try {
270            entryPoints = JSON.parse(existingEntryPointsCookie);
271            if (Array.isArray(entryPoints) && entryPoints.length > 0) {
272                // Сохраняем первую точку входа
273                const firstEntryPoint = entryPoints[0];
274                entryPoints = [firstEntryPoint, currentUrl];
275            } else {
276                entryPoints = [currentUrl];
277            }
278        } catch (e) {
279            entryPoints = [currentUrl];
280        }
281    } else {
282        // Первая точка входа
283        entryPoints = [currentUrl];
284    }
285
286    // Сохраняем в куку
287    CookieUtils.set(COOKIE_ENTRY_POINT, JSON.stringify(entryPoints));
288
289    return true;
290}
291
292// Отправка запроса к Viki API через CORS proxy или напрямую
293async function sendToVikiApi() {
294    try {
295        // Получаем текущую точку входа ИЗ КУКИ (уже в формате без хоста)
296        const entryPointsCookie = CookieUtils.get(COOKIE_ENTRY_POINT);
297        let entryPoint = '';
298
299        if (entryPointsCookie) {
300            try {
301                const entryPoints = JSON.parse(entryPointsCookie);
302                if (Array.isArray(entryPoints) && entryPoints.length > 0) {
303                    // Берем первую точку входа (уже без хоста, например "/forms")
304                    entryPoint = entryPoints[0];
305                }
306            } catch (e) {
307                console.error('Error parsing entry points:', e);
308            }
309        }
310
311        // Добавляем параметры: точку входа (без хоста) и домен
312        const params = new URLSearchParams();
313
314        if (entryPoint) {
315            // entryPoint уже должен быть в формате без хоста, например "/forms"
316            params.append('entry_point', entryPoint);
317        }
318
319        // Добавляем домен текущего сайта
320        params.append('domain', window.location.hostname);
321
322        const urlWithParams = `${VIKI_API_URL}?${params.toString()}`;
323
324        console.log('Sending request to Viki API:', urlWithParams);
325
326        // Стратегия 1: Прямой fetch с mode 'cors' (если сервер поддерживает CORS)
327        try {
328            const response = await fetch(urlWithParams, {
329                method: 'GET',
330                mode: 'cors',
331                headers: {
332                    'Content-Type': 'application/json',
333                    'Accept': 'application/json',
334                },
335                credentials: 'omit',
336            });
337
338            if (!response.ok) {
339                throw new Error(`HTTP error! status: ${response.status}`);
340            }
341
342            const data = await response.json();
343            return handleVikiResponse(data, 'direct fetch');
344
345        } catch (fetchError) {
346            console.log('Direct fetch failed, trying JSONP method:', fetchError.message);
347
348            // Стратегия 2: JSONP (если сервер поддерживает)
349            const jsonpData = await new Promise((resolve) => {
350                const callbackName = 'vikiCallback_' + Date.now();
351
352                const script = document.createElement('script');
353                script.src = `${urlWithParams}&callback=${callbackName}`;
354
355                window[callbackName] = function(data) {
356                    document.head.removeChild(script);
357                    delete window[callbackName];
358
359                    resolve(handleVikiResponse(data, 'JSONP'));
360                };
361
362                script.onerror = function() {
363                    document.head.removeChild(script);
364                    delete window[callbackName];
365                    console.error('JSONP request failed');
366                    resolve(null);
367                };
368
369                document.head.appendChild(script);
370
371                setTimeout(() => {
372                    if (window[callbackName]) {
373                        document.head.removeChild(script);
374                        delete window[callbackName];
375                        console.log('JSONP request timeout');
376                        resolve(null);
377                    }
378                }, 5000);
379            });
380
381            if (jsonpData) {
382                return jsonpData;
383            }
384
385            console.log('JSONP failed, trying proxy method');
386            return sendToVikiApiViaProxy();
387        }
388
389    } catch (error) {
390        console.error('Error sending request to Viki API:', error);
391        return null;
392    }
393}
394
395// Альтернативный метод: отправка через proxy на том же домене
396async function sendToVikiApiViaProxy() {
397    try {
398        // Получаем текущую точку входа
399        const entryPointsCookie = CookieUtils.get(COOKIE_ENTRY_POINT);
400        let entryPoint = '';
401
402        if (entryPointsCookie) {
403            try {
404                const entryPoints = JSON.parse(entryPointsCookie);
405                if (Array.isArray(entryPoints) && entryPoints.length > 0) {
406                    entryPoint = entryPoints[0];
407                }
408            } catch (e) {
409                console.error('Error parsing entry points:', e);
410            }
411        }
412
413        // Отправляем запрос на свой proxy endpoint (нужно настроить на бэкенде)
414        const proxyUrl = '/api/viki-proxy';
415
416        const response = await fetch(proxyUrl, {
417            method: 'POST',
418            headers: {
419                'Content-Type': 'application/json',
420            },
421            body: JSON.stringify({
422                entry_point: entryPoint,
423                domain: window.location.hostname
424            }),
425        });
426
427        if (!response.ok) {
428            throw new Error(`HTTP error! status: ${response.status}`);
429        }
430
431        const data = await response.json();
432        return handleVikiResponse(data, 'proxy');
433
434    } catch (error) {
435        console.error('Error sending request via proxy:', error);
436        return null;
437    }
438}
439
440// Основная функция инициализации
441function initTracking() {
442    // Проверяем, доступен ли доступ к кукам
443    if (!navigator.cookieEnabled) {
444        console.warn('Cookies are disabled. Tracking may not work properly.');
445        return;
446    }
447
448    // Обрабатываем рефер
449    processReferrer();
450
451    // Обрабатываем точку входа (всегда, если есть внешний рефер)
452    processEntryPoint();
453
454    // Если кука уже есть, нормализуем и сохраняем ее как JSON-массив.
455    const hasSegmentCookie = CookieUtils.exists(COOKIE_SEGMENT);
456    const storedSegments = getSegmentsFromCookie();
457
458    if (hasSegmentCookie) {
459        // Сохранение cookie одновременно обновляет ссылки.
460        saveSegments(storedSegments);
461    } else {
462        // Даже без cookie очищаем старые segment/segments из разметки.
463        updateRegLinksWithSegments([]);
464    }
465
466    // Отправляем запрос к Viki API если нет куки сегмента
467    if (!hasSegmentCookie) {
468        sendToVikiApi().then(data => {
469            if (data) {
470                const event = new CustomEvent('segmentDataReceived', { detail: data });
471                window.dispatchEvent(event);
472            }
473        }).catch(error => {
474            console.error('Failed to get segment data:', error);
475        });
476    }
477}
478
479// Проверяем, что мы в браузере
480if (typeof window !== 'undefined' && typeof document !== 'undefined') {
481    // Инициализируем отслеживание при загрузке страницы
482    if (document.readyState === 'loading') {
483        document.addEventListener('DOMContentLoaded', initTracking);
484    } else {
485        initTracking();
486    }
487}
488
489// Экспортируем функции для использования в других модулях (если нужно)
490if (typeof module !== 'undefined' && module.exports) {
491    module.exports = {
492        CookieUtils,
493        isExternalReferrer,
494        processReferrer,
495        processEntryPoint,
496        sendToVikiApi,
497        sendToVikiApiViaProxy,
498        initTracking,
499        normalizeSegments,
500        getSegmentsFromVikiResponse,
501        getSegmentsFromCookie,
502        saveSegments,
503        handleVikiResponse,
504        updateRegLinksWithSegments,
505        COOKIE_DOMAIN,
506        COOKIE_REFER,
507        COOKIE_ENTRY_POINT,
508        COOKIE_SEGMENT,
509        VIKI_API_URL
510    };
511}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.