PageSourceSearch

https://kamelia-med.ru/km_track.js?v=8

js kamelia-med.ru collected 2026-10-07 15:44:17 UTC 27,479 bytes, 475 lines download raw bytes

1/*!
2 * Камелия-Мед — сбор источника обращения и дубль заявок в аналитику.
3 * Vanilla JS, без зависимостей (на kamelia-med.ru jQuery 1.8.3 — намеренно не используем).
4 * Версия 1.0 (2026-07-25)
5 *
6 * Что делает:
7 *   1) при входе на сайт запоминает источник (utm_*, yclid, gclid, реферер, страница входа);
8 *   2) хранит ДВА касания: первое (first) и последнее (last), 90 дней;
9 *   3) берёт ClientID Яндекс.Метрики — второй ключ склейки помимо телефона;
10 *   4) подставляет всё это скрытыми полями в формы сайта (уходит в родное письмо/админку);
11 *   5) при отправке формы шлёт КОПИЮ заявки на приёмник (sendBeacon — не блокирует отправку).
12 *
13 * Родная логика форм не меняется. Любая ошибка внутри скрипта не должна ломать отправку —
14 * поэтому всё в try/catch.
15 *
16 * Подключение (перед </body>):
17 *   <script>window.KM_METRIKA_ID=21948439; window.KM_ENDPOINT='/km-lead.php';</script>
18 *   <script src="/km_track.js" defer></script>
19 */
20(function () {
21  'use strict';
22
23  var CFG = {
24    key: 'km_attr',                                   // ключ в localStorage
25    ttlDays: 90,                                      // окно атрибуции проекта
26    metrikaId: window.KM_METRIKA_ID || null,          // 21948439 (.ru) / 98391964 (.рф)
27    endpoint: window.KM_ENDPOINT || '/km-lead.php',   // приёмник на этом же домене
28    site: window.KM_SITE || location.hostname,
29    debug: !!window.KM_DEBUG
30  };
31
32  function log() { if (CFG.debug && window.console) console.log.apply(console, ['[km_track]'].concat([].slice.call(arguments))); }
33
34  // ---------- хранилище (localStorage с откатом на cookie) ----------
35  function storeGet() {
36    try {
37      var raw = localStorage.getItem(CFG.key);
38      if (!raw) {
39        var m = document.cookie.match(/(?:^|;\s*)km_attr=([^;]+)/);
40        raw = m ? decodeURIComponent(m[1]) : null;
41      }
42      if (!raw) return null;
43      var data = JSON.parse(raw);
44      if (data.exp && data.exp < Date.now()) return null;   // протухло
45      return data;
46    } catch (e) { return null; }
47  }
48
49  function storeSet(data) {
50    try {
51      data.exp = Date.now() + CFG.ttlDays * 864e5;
52      var raw = JSON.stringify(data);
53      try { localStorage.setItem(CFG.key, raw); } catch (e) {}
54      document.cookie = 'km_attr=' + encodeURIComponent(raw) +
55        ';path=/;max-age=' + (CFG.ttlDays * 86400) + ';SameSite=Lax';
56    } catch (e) {}
57  }
58
59  // ---------- разбор источника ----------
60  function qs() {
61    var out = {}, s = location.search.replace(/^\?/, '');
62    if (!s) return out;
63    s.split('&').forEach(function (pair) {
64      if (!pair) return;
65      var i = pair.indexOf('='), k = i < 0 ? pair : pair.slice(0, i);
66      var v = i < 0 ? '' : pair.slice(i + 1);
67      try { out[decodeURIComponent(k)] = decodeURIComponent(v.replace(/\+/g, ' ')); } catch (e) { out[k] = v; }
68    });
69    return out;
70  }
71
72  /** Текущее касание: метки из URL, иначе реферер, иначе прямой заход. */
73  function currentTouch() {
74    var p = qs(), t = { ts: new Date().toISOString(), landing: location.href.slice(0, 500) };
75    ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'].forEach(function (k) {
76      if (p[k]) t[k] = String(p[k]).slice(0, 255);
77    });
78    // клик-идентификаторы рекламных систем
79    ['yclid', 'gclid', 'ysclid', 'fbclid', 'erid'].forEach(function (k) { if (p[k]) t[k] = String(p[k]).slice(0, 255); });
80
81    var ref = document.referrer || '';
82    if (ref) t.referrer = ref.slice(0, 500);
83
84    if (!t.utm_source) {
85      if (t.yclid) { t.utm_source = 'yandex'; t.utm_medium = 'cpc'; }
86      else if (t.gclid) { t.utm_source = 'google'; t.utm_medium = 'cpc'; }
87      else if (ref) {
88        var host = '';
89        try { host = ref.split('/')[2] || ''; } catch (e) {}
90        if (host && host.indexOf(location.hostname) === -1) {
91          t.utm_source = host.replace(/^www\./, '');
92          // поисковики отделяем от обычных ссылок — иначе органика уедет в referral
93          t.utm_medium = /(yandex|google|mail|bing|rambler|duckduckgo)\./i.test(host) ? 'organic' : 'referral';
94        } else { t.utm_source = 'direct'; t.utm_medium = 'none'; }
95      } else { t.utm_source = 'direct'; t.utm_medium = 'none'; }
96    }
97    return t;
98  }
99
100  /** Значимое касание = есть метки/клик-ид или внешний реферер. Прямой заход не перетирает историю. */
101  function isMeaningful(t) {
102    return !!(t.utm_campaign || t.yclid || t.gclid ||
103      (t.utm_source && t.utm_source !== 'direct'));
104  }
105
106  function ensureAttr() {
107    var data = storeGet() || {};
108    var t = currentTouch();
109    if (!data.first) { data.first = t; data.touches = 1; }
110    else if (isMeaningful(t)) {
111      // новое значимое касание: обновляем last, копим цепочку
112      var prev = data.last || data.first;
113      if (prev.utm_source !== t.utm_source || prev.utm_campaign !== t.utm_campaign) {
114        data.last = t;
115        data.touches = (data.touches || 1) + 1;
116        data.chain = ((data.chain || data.first.utm_source || '') + '>' + t.utm_source).slice(-200);
117      }
118    }
119    if (!data.last) data.last = data.first;
120    data.site = CFG.site;
121    storeSet(data);
122    log('attr', data);
123    return data;
124  }
125
126  // ---------- ClientID Метрики ----------
127  function withClientId(cb) {
128    var id = CFG.metrikaId;
129    if (!id || typeof window.ym !== 'function') { cb(''); return; }
130    var done = false;
131    var finish = function (v) { if (!done) { done = true; cb(v || ''); } };
132    try { window.ym(id, 'getClientID', finish); } catch (e) { finish(''); }
133    setTimeout(function () { finish(''); }, 1000);   // Метрика могла не загрузиться — не ждём вечно
134  }
135
136  // ---------- работа с формами ----------
137  var FIELDS = ['km_utm_source', 'km_utm_medium', 'km_utm_campaign', 'km_utm_term',
138                'km_utm_content', 'km_source_first', 'km_ym_cid', 'km_yclid', 'km_landing', 'km_referrer'];
139
140  function setHidden(form, name, value) {
141    if (!value) return;
142    var el = form.querySelector('input[name="' + name + '"]');
143    if (!el) {
144      el = document.createElement('input');
145      el.type = 'hidden';
146      el.name = name;
147      form.appendChild(el);
148    }
149    el.value = String(value).slice(0, 500);
150  }
151
152  /** Скрытые поля с источником — уходят туда же, куда форма (в письмо/админку сайта). */
153  function fillForm(form, attr, ymCid) {
154    try {
155      var L = attr.last || {}, F = attr.first || {};
156      setHidden(form, 'km_utm_source', L.utm_source);
157      setHidden(form, 'km_utm_medium', L.utm_medium);
158      setHidden(form, 'km_utm_campaign', L.utm_campaign);
159      setHidden(form, 'km_utm_term', L.utm_term);
160      setHidden(form, 'km_utm_content', L.utm_content);
161      setHidden(form, 'km_source_first', F.utm_source);
162      setHidden(form, 'km_yclid', L.yclid || L.gclid);
163      setHidden(form, 'km_landing', F.landing);
164      setHidden(form, 'km_referrer', F.referrer);
165      setHidden(form, 'km_ym_cid', ymCid);
166    } catch (e) { log('fillForm error', e); }
167  }
168
169  /** Достаём из формы контакты. Имена полей разные: Drupal submitted[...], Joomla cf_phone. */
170  // Ловушки для ботов (honeypot): скрытое поле, которое человек не видит и не
171  // заполняет, а бот заполняет. CMS такую заявку отклоняет — и мы должны тоже,
172  // иначе спам, отбитый сайтом, всё равно попадёт в 1С администратору.
173  // На камелия-мед.рф (Convert Forms) это поле «eMaiLaddress».
174  function isHidden(el) {
175    try {
176      if (el.type === 'hidden') return true;
177      if (el.offsetParent === null && el.type !== 'hidden') return true;
178      var s = window.getComputedStyle ? window.getComputedStyle(el) : null;
179      if (s && (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) === 0)) return true;
180    } catch (e) {}
181    return false;
182  }
183
184  function honeypotFilled(form) {
185    try {
186      // ТОЛЬКО текстовые поля. Чекбоксы и радио сюда попадать не должны:
187      // их сплошь и рядом прячут ради кастомной стилизации — на kamelia-med.ru
188      // так скрыт чекбокс согласия на обработку ПДн, и он всегда заполнен.
189      // Проверка «любое скрытое непустое = бот» блокировала бы КАЖДУЮ заявку.
190      var TEXTY = { text: 1, email: 1, tel: 1, url: 1, search: 1, number: 1, textarea: 1 };
191      // И ТОЛЬКО ПОЛЯ-ЛОВУШКИ ПО ИМЕНИ. Эвристика «любое скрытое текстовое
192      // заполнено = бот» дважды била по живым полям: 08.08 — по чекбоксу
193      // согласия на ПДн, 10.08 — по textarea «message», которая лежит в
194      // свёрнутом блоке «Указать больше информации» и потому считается
195      // скрытой. Во втором случае заявка пациента, заполнившего комментарий,
196      // молча не отправлялась вообще. Ловушки на наших сайтах известны
197      // поимённо (hp_field у Drupal, eMaiLaddress у Convert Forms на .рф),
198      // поэтому проверяем их, а не всё подряд: пропустить бота дешевле,
199      // чем потерять пациента — спам всё равно отсеет CMS.
200      var TRAP = /^(hp_field|honeypot|hp|winnie|nickname|not_?fill|emailaddress|e_?mail_?address|url|website|company_?url)$/i;
201      var els = form.querySelectorAll('input, textarea');
202      for (var i = 0; i < els.length; i++) {
203        var el = els[i];
204        var type = (el.tagName.toLowerCase() === 'textarea') ? 'textarea' : (el.type || 'text');
205        if (!TEXTY[type]) continue;                          // чекбоксы, radio, hidden — мимо
206        if (!TRAP.test((el.name || '').trim())) continue;    // не ловушка — не трогаем
207        if (!isHidden(el)) continue;
208        if ((el.value || '').trim() !== '') return true;     // ловушка скрыта и заполнена = бот
209      }
210    } catch (e) {}
211    return false;
212  }
213
214  // Подписи полей для карточки 1С. Администратор читает комментарий обращения
215  // глазами, и «clinic: 93» ему ничего не говорит — нужно «Клиника: Роторная».
216  // Имя поля у Drupal приходит как «submitted[clinic]», поэтому сначала
217  // вынимаем последнюю часть. Незнакомое поле оставляем как есть: пусть лучше
218  // будет техническое имя, чем поле молча пропадёт.
219  var FIELD_LABELS = {
220    clinic: 'Клиника', branch: 'Филиал', filial: 'Филиал',
221    service: 'Услуга', services: 'Услуга', usluga: 'Услуга',
222    doctor: 'Врач', vrach: 'Врач', specialist: 'Специалист',
223    speciality: 'Направление', napravlenie: 'Направление',
224    date: 'Дата', data: 'Дата', time: 'Время', vremya: 'Время',
225    date_and_time: 'Желаемое время', time_interval: 'Удобное время',
226    promo: 'Акция', akciya: 'Акция', question: 'Вопрос'
227  };
228  function prettyField(name) {
229    var key = String(name).replace(/^.*\[/, '').replace(/\].*$/, '').toLowerCase();
230    if (FIELD_LABELS[key]) return FIELD_LABELS[key];
231    return String(name).slice(0, 60);
232  }
233
234  function readForm(form) {
235    var out = { fields: {} };
236    var els = form.querySelectorAll('input, textarea, select');
237    for (var i = 0; i < els.length; i++) {
238      var el = els[i];
239      var name = (el.name || '').toLowerCase();
240      var val = (el.value || '').trim();
241      // У выпадающих списков value — это внутренний id справочника Drupal,
242      // и в карточке 1С администратор видел «clinic: 93; service: 18;
243      // doctor: 3736» вместо названий. Берём подпись выбранного пункта:
244      // она и есть то, что человек реально выбрал.
245      if (el.tagName === 'SELECT') {
246        try {
247          var opt = el.options[el.selectedIndex];
248          var optText = opt ? (opt.text || '').trim() : '';
249          // «- Выберите -» и подобные заглушки — не ответ пользователя
250          if (optText && !/^[-–—\s]*(выберите|не выбрано|select|—|-)\s*[-–—\s]*$/i.test(optText)) {
251            // в иерархических списках вложенность рисуют дефисами
252            // («--Лечение кариеса») — администратору это читать незачем
253            val = optText.replace(/^[-–—\s]+/, '');
254          } else if (optText) {
255            continue;
256          }
257        } catch (e) {}
258      } else if (el.type === 'radio' || el.type === 'checkbox') {
259        // Переключатели — та же беда, что у списков: value это id справочника
260        // («clinic: 92»). Подпись Drupal кладёт в отдельный label[for], а не
261        // оборачивает поле, поэтому сначала ищем по id и только потом
262        // смотрим на обёртку и соседа.
263        try {
264          var lbl = el.id ? document.querySelector('label[for="' + el.id + '"]') : null;
265          if (!lbl) lbl = el.closest('label');
266          if (!lbl && el.nextElementSibling && el.nextElementSibling.tagName === 'LABEL') {
267            lbl = el.nextElementSibling;
268          }
269          var lblText = lbl ? (lbl.innerText || lbl.textContent || '').trim() : '';
270          if (lblText) val = lblText.slice(0, 120);
271        } catch (e) {}
272      }
273      if (!name || !val) continue;
274      if (el.type === 'hidden' && name.indexOf('km_') !== 0) continue;      // служебные поля CMS не тащим
275      if (el.type === 'checkbox' && !el.checked) continue;
276      if (el.type === 'radio' && !el.checked) continue;
277      // Служебное в комментарий не тащим: в 1С у обращения оно выглядит мусором
278      // («agree: 1; op: Перезвоните мне» вместо слов пациента) — поймано на живой
279      // форме 08.08.2026. Значимые поля (сообщение, услуга, филиал) проходят.
280      if (/pass|token|captcha|form_build|form_id|form_mode|honeypot|h
280p_field|not.?fill|^agree$|^op$|^submit$|consent|privacy|^source\[|time_interval|^sy_|^l10n$|^reqenc$|^searchid$/i.test(name)) continue;
281
282      if (!out.phone && (/phone|tel|mobil|телефон/i.test(name) || el.type === 'tel')) out.phone = val;
283      else if (!out.email && (/mail/i.test(name) || el.type === 'email')) out.email = val;
284      else if (!out.name && /(^|\[)(name|fio|имя)/i.test(name) && !/username|form/i.test(name)) out.name = val;
285      else if (!out.comment && /(comment|message|text|вопрос|коммент)/i.test(name)) out.comment = val;
286      else if (name.indexOf('km_') !== 0) out.fields[prettyField(name)] = val.slice(0, 200);
287    }
288    // Запасной вариант: телефон по маске, если поле названо непривычно.
289    //
290    // ВАЖНО, чем он был опасен. Раньше здесь перебирались ВСЕ элементы формы,
291    // включая скрытые, и первое значение из 10+ цифр объявлялось телефоном.
292    // А в скрытых полях лежат utm_content (id объявления Директа), yclid и
293    // прочие числа ровно той же длины. Когда человек кликал кнопку на пустой
294    // форме, трекер (он срабатывает на mousedown, до валидации браузера)
295    // отправлял в 1С заявку, которой не было, с «номером» вида
296    // +7 (5782) 659870 — это был id объявления. За 08–10.08 так ушло
297    // 9 фиктивных обращений из 40 — четверть потока, и администратор
298    // тратил на них время, пытаясь дозвониться.
299    //
300    // Поэтому: только видимые поля ввода, тот же фильтр служебных имён, что и
301    // выше, и проверка на реальный российский номер. Если валидного телефона
302    // нет — оставляем пусто: заявка без контактов не отправится вовсе, и это
303    // честнее, чем выдумать номер.
304    if (!out.phone) {
305      for (var j = 0; j < els.length; j++) {
306        var fb = els[j];
307        if (fb.type === 'hidden' || fb.type === 'checkbox' || fb.type === 'radio') continue;
308        var fbName = (fb.name || '').toLowerCase();
309        if (/pass|token|captcha|form_build|form_id|form_mode|honeypot|hp_field|not.?fill|consent|privacy|^sy_|^l10n$|^reqenc$|^searchid$/i.test(fbName)) continue;
310        var v = (fb.value || '').trim();
311        if (!/^[\d\s()+-]{10,20}$/.test(v)) continue;
312        var dg = v.replace(/\D/g, '');
313        if (dg.length < 10 || dg.length > 11) continue;
314        // код региона/оператора в России начинается на 3, 4, 8 или 9;
315        // id объявления (5782659870) на этом и отсекается
316        if ('3489'.indexOf(dg.slice(-10).charAt(0)) < 0) continue;
317        out.phone = v;
318        break;
319      }
320    }
321    return out;
322  }
323
324  // Человеческое название формы для карточки в 1С: администратору «eforms-callme-form»
325  // ни о чём не говорит, а «Обратный звонок» сразу объясняет, откуда заявка.
326  // Берём со страницы, а не из словаря — тогда новые формы подписываются сами.
327  function formTitle(form) {
328    // «Отправить» и «ОК» в карточке 1С ничего не объясняют — если кнопка подписана
329    // так, продолжаем искать заголовок выше по дереву (на камелия-мед.рф форма
330    // лежит в модалке UIkit, и осмысленное название только там).
331    var GENERIC = /^(отправить|отправка|ок|ok|submit|send|готово|далее)$/i;
332    var SEL = 'legend, .form-title, .modal-title, .uk-modal-title, h1, h2, h3, h4, h5';
333    var t = '';
334    try {
335      var h = form.querySelector(SEL);
336      if (!h && form.closest) {
337        var box = form.closest('.modal, .popup, .modal-content, .uk-modal, .uk-modal-dialog, section, .block, .b-form');
338        if (box) h = box.querySelector(SEL);
339      }
340      t = (h ? (h.textContent || '') : '').replace(/\s+/g, ' ').trim();
341      if (!t || GENERIC.test(t)) {
342        var btn = form.querySelector('button[type="submit"], input[type="submit"], button');
343        var bt = (btn ? (btn.textContent || btn.value || '') : '').replace(/\s+/g, ' ').trim();
344        // берём текст кнопки, только если он что-то говорит; иначе оставляем заголовок
345        if (bt && !GENERIC.test(bt)) t = bt;
346        else if (!t) t = bt;
347      }
348    } catch (e) {}
349    return t.replace(/\s+/g, ' ').trim().slice(0, 60);
350  }
351
352  function sendCopy(form, attr, ymCid) {
353    try {
354      if (honeypotFilled(form)) { log('заполнена ловушка для ботов — не шлём'); return; }
355      var data = readForm(form);
356      if (!data.phone && !data.email) { log('нет контактов — не шлём'); return; }
357
358      var L = attr.last || {}, F = attr.first || {};
359      var payload = {
360        v: 1,
361        site: CFG.site,
362        page: location.href.slice(0, 500),
363        form_id: form.getAttribute('id') || form.getAttribute('name') || '',
364        form_title: formTitle(form),
365        name: data.name || '',
366        phone: data.phone || '',
367        email: data.email || '',
368        comment: data.comment || '',
369        extra: data.fields,
370        utm_source: L.utm_source || '', utm_medium: L.utm_medium || '',
371        utm_campaign: L.utm_campaign || '', utm_term: L.utm_term || '',
372        utm_content: L.utm_content || '',
373        yclid: L.yclid || '', gclid: L.gclid || '',
374        first_source: F.utm_source || '', first_campaign: F.utm_campaign || '',
375        chain: attr.chain || '', touches: attr.touches || 1,
376        landing: F.landing || '', referrer: F.referrer || '',
377        ym_cid: ymCid || '',
378        ts: new Date().toISOString()
379      };
380      var body = JSON.stringify(payload);
381      var ok = false;
382      // sendBeacon переживает переход/редирект после сабмита — то, что нужно
383      if (navigator.sendBeacon) {
384        try { ok = navigator.sendBeacon(CFG.endpoint, new Blob([body], { type: 'text/plain;charset=UTF-8' })); } catch (e) {}
385      }
386      if (!ok) {
387        try {
388          var x = new XMLHttpRequest();
389          x.open('POST', CFG.endpoint, true);
390          x.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8');
391          x.send(body);
392        } catch (e) { log('send error', e); }
393      }
394      log('копия отправлена', payload);
395    } catch (e) { log('sendCopy error', e); }
396  }
397
398  // ---------- запуск ----------
399  function init() {
400    var attr = ensureAttr();
401    withClientId(function (ymCid) {
402      try { if (ymCid) { attr.ym_cid = ymCid; storeSet(attr); } } catch (e) {}
403
404      var fill = function () {
405        var forms = document.querySelectorAll('form');
406        for (var i = 0; i < forms.length; i++) {
407          if (forms[i].getAttribute('data-km') === '1') continue;
408          if (forms[i].querySelector('input[type="search"]') && forms[i].querySelectorAll('input').length < 3) continue; // поиск пропускаем
409          forms[i].setAttribute('data-km', '1');
410          fillForm(forms[i], attr, ymCid);
411        }
412      };
413      fill();
414
415      // формы могут появляться позже (модалки, поп-апы)
416      try {
417        new MutationObserver(function () { fill(); })
418          .observe(document.documentElement, { childList: true, subtree: true });
419      } catch (e) {}
420
421      // Одна отправка на форму, из какого бы обработчика ни пришли.
422      function handoff(form) {
423        if (!form || form.tagName !== 'FORM') return;
424        if (form.getAttribute('data-km-sent') === '1') return;
425        form.setAttribute('data-km-sent', '1');
426        setTimeout(function () { form.removeAttribute('data-km-sent'); }, 3000);
427        fillForm(form, attr, ymCid);
428        sendCopy(form, attr, ymCid);
429      }
430
431      // (1) нативный submit, capture — срабатываем раньше обработчиков CMS
432      document.addEventListener('submit', function (ev) {
433        try { handoff(ev.target); } catch (e) { log('submit handler error', e); }
434      }, true);
435
436      // (2) клик по кнопке отправки.
437      // Обязательно: формы Drupal на этом сайте уходят через AJAX (use-ajax,
438      // ajax-eforms-callme-form), а jQuery-триггер submit не всплывает до document
439      // нативным событием — на одном обработчике submit заявки терялись бы молча.
440      // Тем же способом ловит формы виджет Манго (mousedown по form .btn).
441      var onPress = function (ev) {
442        try {
443          var el = ev.target;
444          for (var d = 0; el && d < 5; d++, el = el.parentNode) {
445            if (!el.tagName) continue;
446            var tag = el.tagName.toLowerCase();
447            var type = (el.getAttribute && (el.getAttribute('type') || '')).toLowerCase();
448            var isBtn = tag === 'button' || (tag === 'input' && (type === 'submit' || type === 'button'))
449                        || (el.className && /\bbtn\b|submit/i.test(el.className));
450            if (!isBtn) continue;
451            var form = el.form || (el.closest && el.closest('form'));
452            if (form) { handoff(form); }
453            return;
454          }
455        } catch (e) { log('press handler error', e); }
456      };
457      document.addEventListener('mousedown', onPress, true);
458      document.addEventListener('touchstart', onPress, true);
459    });
460  }
461
462  try {
463    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
464    else init();
465  } catch (e) { /* молча: скрипт не должен ломать сайт */ }
466})();
467
468/* Квиз «Имплантация», подключение. Добавлено 28.08.2026.
469   Живёт здесь, а не в counters.inc: тот лежит в теме, а тему переписывает
470   деплой Цетеры целиком (проверено: 25.08 15:35 переписаны themes/, modules/,
471   sites/, а файлы в корне уцелели). Обе строки в try/catch — сбой квиза
472   не должен задеть отправку заявок. Путь проверяется, чтобы файлы
473   не грузились на остальных страницах сайта. */
474try { if (location.pathname.indexOf('/implantaciya-zubov') === 0) { var kmqL = document.createElement('link'); kmqL.rel = 'stylesheet'; kmqL.href = '/kviz.css?v=1'; document.head.appendChild(kmqL); } } catch (e) {}
475try { if (location.pathname.indexOf('/implantaciya-zubov') === 0) { var kmqS = document.createElement('script'); kmqS.src = '/kviz.js?v=2'; document.head.appendChild(kmqS); } } catch (e) {}

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.