PageSourceSearch

https://tvoe.ru/assets/js/metrika.js

js tvoe.ru collected 2026-10-03 20:47:18 UTC 11,826 bytes, 219 lines download raw bytes

1// Мост между Dart-кодом сайта и Яндекс.Метрикой.
2//
3// ЗАЧЕМ ОТДЕЛЬНЫМ СКРИПТОМ. Файлы `lib/client/**` собираются и в браузерный
4// JS, и в серверную AOT-сборку, поэтому объявлять в них внешние JS-функции
5// (`@JS ... external`) нельзя. Тот же приём уже используется картой магазинов:
6// Dart шлёт DOM-событие, скрипт его слушает.
7//
8// ОЧЕРЕДЬ ОБЯЗАТЕЛЬНА. Счётчик инициализируется асинхронно, а события
9// начинаются с первого кадра страницы. Прямой вызов `ym(...)` на холодной
10// загрузке падает — в мобильном вебе это ловилось как `NoSuchMethodError`
11// (см. `lib/di/yandex_web_metrika/js_ecom_web.dart`). Копим и сливаем.
12//
13// ЗДЕСЬ ЖЕ ПОДНИМАЕТСЯ САМ СЧЁТЧИК (`boot`). Владелец Метрики в браузере
14// один — этот файл: он и грузит `tag.js`, и зовёт `init`, и держит очередь.
15// Разнеси это по двум местам — и порядок «init раньше flush» пришлось бы
16// поддерживать вручную из разметки.
17//
18// ПОДЪЁМ ОТЛОЖЕН — как в мобильном вебе (`web/index.html`, там это
19// `flutter-first-frame` + 1000 мс). `tag.js` с вебвизором тянет заметный
20// сторонний JS, а LCP сайта вылизан до 0.45 с; в критический путь его пускать
21// нельзя. Терять события отсрочка не заставляет — их держит очередь выше,
22// а `hit` на страницу входа Метрика засчитывает сама при `init`.
23(function () {
24  var queue = [];
25  var counterId = 0;
26  var booted = false;
27  var scheduled = false;
28  var bootTimer = 0;
29  var disabled = document.cookie.split(';').some(function (part) {
30    return part.trim() === 'tvoe_cookie_consent=rejected';
31  });
32
33  // Подъём счётчика: сниппет Яндекса + `init`. Зовётся ровно один раз.
34  //
35  // Параметры `init` — те же, что в мобильном вебе (`web/index.html`):
36  // разойдись они, и отчёты сайта с приложением перестанут сходиться.
37  // `referrer`/`url` сюда НЕ передаются: `init` их не читает (это аргументы
38  // метода `hit`), а просмотры сайт шлёт сам — `AnalyticsRouteObserver`
39  // (`lib/client/analytics/route_observer.dart`) с честными url и referer.
40  function boot() {
41    if (disabled || booted || counterId <= 0) return;
42    booted = true;
43    (function (m, e, t, r, i, k, a) {
44      m[i] = m[i] || function () { (m[i].a = m[i].a || []).push(arguments) };
45      m[i].l = 1 * new Date();
46      for (var j = 0; j < document.scripts.length; j++) { if (document.scripts[j].src === r) { return; } }
47      k = e.createElement(t), a = e.getElementsByTagName(t)[0], k.async = 1, k.src = r, a.parentNode.insertBefore(k, a);
48    })(window, document, 'script', 'https://mc.yandex.ru/metrika/tag.js?id=' + counterId, 'ym');
49
50    // `dataLayer` — ДО `init`: Метрика вычитывает уже накопленный массив в
51    // момент инициализации, и товарные события, отправленные до подъёма
52    // счётчика, не теряются.
53    window.dataLayer = window.dataLayer || [];
54    window.ym(counterId, 'init', {
55      ssr: true,
56      webvisor: true,
57      clickmap: true,
58      ecommerce: 'dataLayer',
59      accurateTrackBounce: true,
60      trackLinks: true
61    });
62
63    // `ClientID` — Mindbox: он хранит его кастом-полем клиента
64    // (`website.EditYandexClientId`). Отдаём DOM-событием, чтобы этот файл
65    // остался единственным владельцем `window.ym`.
66    try {
67      window.ym(counterId, 'getClientID', function (clientId) {
68        if (!clientId) return;
69        document.dispatchEvent(new CustomEvent('tv-metrika-client-id', { detail: String(clientId) }));
70      });
71    } catch (e) {
72      // Метрика могла не подняться — Mindbox проживёт без этого поля.
73    }
74
75    // Сниппет выше создаёт `window.ym` синхронно (заглушка с очередью `.a`),
76    // поэтому очередь можно сливать сразу — `tag.js` доедет и разберёт её сам.
77    flush();
78  }
79
80  // Планировщик подъёма. `load`, а не `DOMContentLoaded`: к моменту `load`
81  // картинки первого экрана уже нарисованы, и сторонний JS ни с чем не
82  // конкурирует. Секунда сверху — тот же зазор, что в мобильном вебе.
83  function schedule() {
84    if (disabled) return;
85    // Номер приезжает дважды (инлайн в разметке и чтение `__tvCounterId` этим
86    // файлом) — планируем ровно один раз. Сам `boot` тоже защищён (`booted`),
87    // но копить отменённые таймеры незачем.
88    if (scheduled) return;
89    scheduled = true;
90    if (document.readyState === 'complete') {
91      bootTimer = setTimeout(boot, 1000);
92      return;
93    }
94    window.addEventListener('load', function () {
95      if (!disabled) bootTimer = setTimeout(boot, 1000);
96    }, { once: true });
97  }
98
99  // Поднять счётчик НЕМЕДЛЕННО, не дожидаясь `load` + 1 с.
100  //
101  // ЗАЧЕМ. Обычная отсрочка (см. `schedule`) правильна для витрины и каталога:
102  // аналитика не должна конкурировать с первым экраном. Но на оформлении
103  // заказа она становится частью сценария: человек нажимает «Оформить», и
104  // страница тут же уходит. Если к этому моменту `tag.js` ещё не поднялся,
105  // цель `purchase` остаётся в очереди, подтверждения не будет никогда, а
106  // товарная часть уедет из `dataLayer` вместе с док
106ументом.
107  //
108  // Симптом заказчика (04.09.2026): на одном ПК `purchase` в дебаггере не
109  // появился вовсе. На проде замерено: `window.ym` не существует и через
110  // 7 секунд после начала загрузки страницы товара — `load` там наступает
111  // поздно.
112  //
113  // Идемпотентна: `boot` защищён флагом `booted`, повторные вызовы бесплатны.
114  window.tvAnalyticsBoot = function () {
115    if (disabled) return;
116    scheduled = true;
117    boot();
118  };
119
120  window.tvAnalyticsReject = function () {
121    disabled = true;
122    queue.length = 0;
123    if (bootTimer) clearTimeout(bootTimer);
124    try {
125      if (counterReady()) window.ym(counterId, 'destruct');
126    } catch (e) {
127      // Счётчик уже мог исчезнуть сам; отказ всё равно считается применённым.
128    }
129  };
130
131  // То же самое событием — им пользуется Dart (`analytics.dart#
132  // bootAnalyticsCounter`), чтобы не трогать `window` напрямую.
133  document.addEventListener('tv-analytics-boot', function () {
134    window.tvAnalyticsBoot();
135  });
136
137  function counterReady() {
138    return counterId > 0 && typeof window.ym === 'function';
139  }
140
141  function send(item) {
142    if (item.kind === 'ecommerce') {
143      window.dataLayer = window.dataLayer || [];
144      window.dataLayer.push({ ecommerce: item.ecommerce });
145      return;
146    }
147    if (item.kind === 'hit') {
148      window.ym(counterId, 'hit', item.url, { referer: item.referer });
149      return;
150    }
151    // `ackId` — только у цели `purchase` оформления заказа
152    // (`yandex_metrika_sink.dart#reportPurchaseAwaitingAck`): сайт уводит
153    // браузер со страницы жёстким переходом сразу после отправки, и без
154    // подтверждения `reachGoal`'а колбэком запрос к `mc.yandex.ru` рискует
155    // оборваться на середине, не дождавшись сети. Колбэк — штатный четвёртый
156    // аргумент `reachGoal`, задокументированный самой Метрикой; остальные
157    // цели его не просят и посылаются как раньше, без колбэка.
158    if (item.ackId) {
159      window.ym(counterId, 'reachGoal', item.name, item.params, function () {
160        document.dispatchEvent(new CustomEvent('tv-analytics-ack', { detail: item.ackId }));
161      });
162      return;
163    }
164    window.ym(counterId, 'reachGoal', item.name, item.params);
165  }
166
167  function flush() {
168    if (!counterReady()) return;
169    while (queue.length) {
170      try {
171        send(queue.shift());
172      } catch (e) {
173        // Одно сломанное событие не имеет права запереть очередь.
174      }
175    }
176  }
177
178  // Приём номера счётчика из разметки. Идемпотентна: разметка зовёт её
179  // инлайном, а этот файл — сам при загрузке (см. низ), и повторный вызов не
180  // должен ни задваивать `tag.js`, ни перепланировать подъём (`booted`).
181  window.tvAnalyticsInit = function (id) {
182    if (disabled) return;
183    if (typeof id !== 'number' || id <= 0) return;
184    counterId = id;
185    // Счётчика ещё нет — `flush` здесь ничего не сделает (`counterReady`
186    // требует живого `window.ym`). Очередь уедет из `boot`.
187    schedule();
188  };
189
190  document.addEventListener('tv-analytics', function (event) {
191    if (disabled) return;
192    var item;
193    try {
194      item = JSON.parse(event.detail);
195    } catch (e) {
196      return;
197    }
198    // Ecommerce не требует готовности счётчика: `dataLayer` — обычный массив,
199    // Метрика вычитывает его при инициализации.
200    if (item.kind === 'ecommerce') {
201      send(item);
202      return;
203    }
204    queue.push(item);
205    flush();
206  });
207
208  document.addEventListener('tv-cookie-consent-rejected', function () {
209    window.tvAnalyticsReject();
210  });
211
212  // Номер мог приехать раньше, чем загрузился этот файл: разметка выставляет
213  // `window.__tvCounterId` инлайном в `<head>`, а сам файл идёт с `defer` и
214  // выполняется после разбора документа. Это, а не инлайновый вызов, —
215  // ОСНОВНОЙ путь получения номера; вызов в разметке оставлен страховкой.
216  if (!disabled && typeof window.__tvCounterId === 'number') {
217    window.tvAnalyticsInit(window.__tvCounterId);
218  }
219})();

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.