PageSourceSearch

https://portaprima.ru/local/templates/portaprima/js/catalogAnalytics.js

js portaprima.ru collected 2026-10-06 07:28:22 UTC 20,035 bytes, 663 lines download raw bytes

1// Аналитика каталога: Яндекс.Метрика / dataLayer, VK Retargeting, GA e-commerce.
2// Вынесено из commonCatalog_new.js: это независимый слой, каталог работает и без него
3// (например, при ?analitic_scripts=disable_all файл можно просто не подключать -
4// в commonCatalog_new.js есть заглушка $$.metrika, поэтому вызовы не упадут).
5//
6// Связь односторонняя: каталог зовёт $$.metrika.*, а vk_ecomm/ga_ecomm дёргает уже она сама.
7
8var $$ = $$ || {};
9
10// --- Мини-хелперы вместо jQuery ---------------------------------------------------
11// Каталог (commonCatalog_new.js) пока не мигрирован и передаёт сюда jQuery-объекты,
12// поэтому вход нормализуем: принимаем и jQuery-обёртку, и обычный DOM-элемент.
13function anEl(node) {
14	if (!node) return null;
15	return node.jquery ? node[0] : node;
16}
17
18function anEls(nodes) {
19	if (!nodes) return [];
20	if (nodes.jquery) return Array.prototype.slice.call(nodes.get());
21	if (nodes instanceof Element) return [nodes];
22	return Array.prototype.slice.call(nodes);
23}
24
25// ВАЖНО: jQuery .data() приводит типы ("12345" -> 12345, "true" -> true), а dataset
26// отдаёт строки. Для dataLayer/GA цена обязана быть числом, иначе e-commerce ломается.
27// Повторяем поведение jQuery.
28function anData(node, key) {
29	var el = anEl(node);
30	if (!el) return undefined;
31
32	var raw = el.getAttribute('data-' + key);
33	if (raw === null) return undefined;
34	if (raw === '') return '';
35	if (raw === 'true') return true;
36	if (raw === 'false') return false;
37	if (raw === 'null') return null;
38
39	// Число - только если строка ровно им и является (иначе "8 800..." станет 8).
40	var num = Number(raw);
41	if (raw.trim() !== '' && !isNaN(num) && String(num) === raw.trim()) return num;
42
43	if (raw[0] === '{' || raw[0] === '[') {
44		try { return JSON.parse(raw); } catch (e) { /* не JSON - вернём строкой */ }
45	}
46	return raw;
47}
48
49function anSetData(node, key, value) {
50	var el = anEl(node);
51	if (el) el.setAttribute('data-' + key, value);
52}
53// ----------------------------------------------------------------------------------
54
55$$._metrika = function(){
56	// this.id = 22603441;
57	this.id = '';
58	this.YaCounters = [];
59	
60	this.selectors = {
61		product : '.js-datalayer-product',
62		product_click : '.js-datalayer-product-click'
63	};
64	
65	this.products = [];
66	
67	window.dataLayer = window.dataLayer || [];
68	
69	var self = this;
70	
71	//topik [25.01.2021]: Метод отправляет заявку Comagic, только когда сам скрипт Comagic подключен.
72	//	Событие может отправляться при загрузке страницы, но скрипт сервиса в этот момент может быть ещё не загружен.
73	//-----{
74	this.ComagicAddRequest = function(obj_raw){
75		var obj = {name: '', email: '', phone: '', message: ''};
76		for(var i in obj_raw) obj[i] = obj_raw[i];
77		
78		if(typeof Comagic !== 'undefined' && typeof Comagic.addOfflineRequest === 'function'){
79			Comagic.addOfflineRequest(obj);
80			console.log(obj);
81		}
82		else{
83			setTimeout(function(){ this.ComagicAddRequest(obj); }.bind(this), 700);
84		}
85	}
86	//-----}
87	
88	//topik [16.08.2020]: Метод для получения номера текущего счетчика Яндекс.Метрики.
89	//	По умолчанию - номер счетчика основного сайта (22603441).
90	//topik [29.09.2020]: Теперь счетчиков может быть несколько.
91	//	Поэтому собираем их все и каждое событие вызываем для каждого счетчика.
92	//-----{
93	this.waitForYm = function(){
94		let metrikaObj = (window.Ya && (window.Ya.Metrika || window.Ya.Metrika2)) || null;
95		ymCounterNum = (metrikaObj && metrikaObj.counters && (metrikaObj.counters() || [0])[0].id) || 0;
96
97		let ymCounterObj = window['yaCounter' + ymCounterNum] || null;
98		if(ymCounterObj){
99			this.id = ymCounterNum;
100			this.YaCounters = metrikaObj.counters();
101			// console.log('waitForYm', this.id, this.YaCounters);
102			/*
103			$.each(this.YaCounters, function(el){
104				console.log(el.id);
105			});
106			*/
107			return;
108		}
109		
110		this.waitYmTimeCnt = (this.waitYmTimeCnt || 0) + 1;
111		if(this.waitYmTimeCnt > 20) return;
112		else if(this.waitYmTimeCnt > 15) interval = 1000;
113		else if(this.waitYmTimeCnt > 8) interval = 500;
114		else interval = 250;
115		
116		setTimeout(function(){ this.waitForYm(); }.bind(this), interval);
117	}
118	this.waitForYm();
119	//-----}
120	
121	this.pushEvent = function(event_code, params = {}){
122		if(!event_code) return false;
123		
124		action = params.action || 'click';
125		label = params.label || '';
126		
127		if(typeof ga !== 'undefined'){
128			ga('send', 'event', event_code, action, label);
129			console.log('GA: ' + event_code + ' [action:' + action + '; label:' + label + ']');
130		}
131		
132		if(typeof ym !== 'undefined'){
133			if (this.id) {
134				ym(this.id, 'reachGoal', event_code);
135			}
136			//console.log('YM: ' + event_code + ' - ' + this.id);
137		}
138		
139		if(typeof _txq !== 'undefined'){
140			event_code_txq = false;
141			
142			if(event_code == 'add_v_korziny_kartochka') event_code_txq = 'basket';
143			else if(event_code == 'door_turnkey') event_code_txq = 'door';
144			else if(event_code == 'ask_partner_corp') event_code_txq = 'question_partner';
145			else if(event_code == 'pochtabank_form_send') event_code_txq = 'installment_plan';
146			else if(event_code == 'zakaz_peregorodki') event_code_txq = 'interior_partition';
147			else if(event_code == 'vyzov_zamer') event_code_txq = 'gauge';
148			else if(event_code == 'zakaz_zvonok') event_code_txq = 'call_back';
149			else if(event_code == 'ask_partner_franshiza') event_code_txq = 'franchise';
150			else if(event_code == 'order_processed') event_code_txq = 'order_processed';
151			
152			if(event_code_txq){
153				_txq.push(['track', event_code_txq]);
154				//console.log('TXQ: ' + event_code_txq);
155			}
156		}
157	}
158	
159	this.formatProductsArray = function(products, action){
160		Object.keys(products).forEach(function(i){
161			var product = {},
162				product_f = {};
163
164			// В products приходят либо готовые объекты (из ajax-ответа), либо DOM/jQuery-узлы.
165			var node = products[i];
166			var isNode = node && (node.jquery || node instanceof Element);
167			if(!isNode) product = node;
168			else{
169				product = {
170					'id': anData(node, 'id'),
171					'name' : anData(node, 'name'),
172					'price': anData(node, 'price'),
173					'brand': anData(node, 'brand'),
174					'category': anData(node, 'section-path'),
175					'variant' : anData(node, 'size')
176				};
177			} 
178			
179			if(product.id) product_f.id = product.id;
180			if(product.name) product_f.name = product.name;
181			if(action != 'remove' && product.price) product_f.price = product.price;
182			if(action != 'remove') product_f.brand = product.brand ? product.brand : self.brand;
183			if(product.category) product_f.category = product.category;
184			if(action != 'remove' && product.variant) product_f.variant = product.variant;
185			if(action != 'remove' && product.quantity) product_f.quantity = product.quantity;
186			
187			products[i] = product_f;
188		});
189		return products;
190	}
191	
192	this.viewProduct = function($product){
193		product = this.initProductData($product, 'detail');
194		
195		console.log(
196			"event: detail\n" + 
197			"name: "  + product.name + "\n" + 
198			"id: "  + product.id + "\n" + 
199			"price: "  + product.price + "\n" + 
200			"brand: "  + product.brand + "\n" + 
201			"category: "  + product.category + "\n" + 
202			"variant: "  + product.variant + "\n" + 
203			"list: "  + (BX.getCookie('portaprimaMetrikaProductList') || '') + "\n" + 
204			"position: "  + product.position
205		);
206		
207		BX.setCookie('portaprimaMetrikaProductList', false, {expires: -1, path: '/'});
208
209
210		// tooltipster - jQuery-плагин и к аналитике отношения не имеет (исторический хвост).
211		// Зовём, только если jQuery с плагином на странице есть: файл от них не зависит.
212		if (window.jQuery && window.jQuery.fn && window.jQuery.fn.tooltipster) {
213			window.jQuery(".tooltip").tooltipster({
214				theme: "tooltipster-my-shadow",
215				maxWidth: 200,
216				// trigger: "click",
217				contentAsHTML: true
218			});
219		}
220
221		/*
222		window.dataLayer.push({
223			'ecommerce': {
224				'detail': {
225					'products': this.formatProductsArray(products, 'detail')
226				}
227			}
228		});
229		*/
230		if ($$.vk_ecomm) {
231			$$.vk_ecomm.viewProduct(product);
232		}
233		if ($$.ga_ecomm) {
234			$$.ga_ecomm.viewProduct(product);
235		}
236	}
237	
238	this.addBasketProduct = function(products){
239		/*
240		window.dataLayer.push({
241			'ecommerce': {
242				'add': {
243					'products': this.formatProductsArray(products, 'add')
244				}
245			}
246		});
247		*/
248		if ($$.vk_ecomm) {
249			$$.vk_ecomm.addBasketProduct(products);
250		}
251		if ($$.ga_ecomm) {
252			$$.ga_ecomm.addBasketProduct(products);
253		}
254	}
255	
256	this.removeBasketProduct = function(products){
257		/*
258		window.dataLayer.push({
259			'ecommerce': {
260				'remove': {
261					'products': this.formatProductsArray(products, 'remove')
262				}
263			}
264		});
265		*/
266		if ($$.vk_ecomm) {
267			$$.vk_ecomm.removeBasketProduct(products);
268		}
269		if ($$.ga_ecomm) {
270			$$.ga_ecomm.removeBasketProduct(products);
271		}
272	}
273	
274	this.purchaseOrder = function(id, sum, products){
275		/*
276		window.dataLayer.push({
277			'ecommerce': {
278				'purchase': {
279					'actionField': { 'id': id },
280					'products': this.formatProductsArray(products, 'purchase')
281				}
282			}
283		});
284		*/
285		if ($$.vk_ecomm) {
286			$$.vk_ecomm.purchaseOrder(id, products);
287		}
288		if ($$.ga_ecomm) {
289			$$.ga_ecomm.purchaseOrder(id, sum, products);
290		}
291	}
292	
293	this.clickProduct = function(product){
294		if (!product) {
295			return;
296		}
297		console.log(
298			"event: click\n" + 
299			"name: "  + product.name + "\n" + 
300			"id: "  + product.id + "\n" + 
301			"price: "  + product.price + "\n" + 
302			"brand: "  + product.brand + "\n" + 
303			"category: "  + product.category + "\n" + 
304			"variant: "  + product.variant + "\n" + 
305			"list: "  + product.list + "\n" + 
306			"position: "  + product.position
307		);
308		
309		BX.setCookie('portaprimaMetrikaProductList', product.list, {expires: 60*1, path: '/'});
310	}
311	
312	this.productImpressions = function(products){
313		// console.log('list: ' + products.length);
314
315		for (key in products)
316		{
317			let item = products[key];
318			let itemID = item.id;
319			let itemBlock = document.querySelector('.js-datalayer-product[data-layer-id="'+itemID+'"]');
320			let itemLink = itemBlock && itemBlock.querySelector(':scope > a.js-datalayer-product-click');
321			let itemUrl = itemLink ? itemLink.getAttribute('href') : '';
322
323			itemUrl = BX.util.add_url_param(itemUrl, {'mode': 'quickview'});
324		}
325		if ($$.ga_ecomm) {
326			$$.ga_ecomm.viewProducts(products);
327		}
328		// console.log('productImpressions', products);
329	}
330	
331	this.initProductData = function($product, action){
332		var node = anEl($product);
333		var product = {
334			'name' : (anData(node, 'layer-name') || (anData(node, 'name') || '')),
335			'id' : (anData(node, 'layer-id') || (anData(node, 'id') || '')),
336			'price' : (anData(node, 'layer-price') || (anData(node, 'price') || '')),
337			'brand' : (anData(node, 'layer-brand') || (anData(node, 'brand') || '')),
338			'category' : (anData(node, 'layer-category') || (anData(node, 'section-path') || '')),
339			'variant' : (anData(node, 'layer-variant') || (anData(node, 'variant') || '')),
340			'list' : (anData(node, 'layer-list') || window.location.pathname),
341			'position' : (anData(node, 'layer-position') || (anData(node, 'position') || ''))
342		};
343		
344		if(action == 'detail') delete product.list;
345		if(action == 'detail') delete product.position;
346		
347		return product;
348	}
349	
350	this.productListInit = function($list){
351		var list = anEl($list);
352		if(!list) return undefined;
353
354		if(!anData(list, 'layer-inited')){
355			var list_id = Math.floor(Math.random() * 10000);
356
357			self.products[list_id] = [];
358
359			// Делегирование внутри списка (было $list.on('click', selector, fn)).
360			list.addEventListener('click', function(e){
361				var target = e.target.closest(self.selectors.product_click);
362				if (target && list.contains(target)) {
363					self.clickProduct(self.products[list_id][anData(target, 'layer-id')]);
364				}
365			});
366
367			// jQuery .data(key, value) писал во ВНУТРЕННЕЕ хранилище, а не в DOM.
368			// Пишем в data-атрибут: читается тем же anData(), заодно видно в инспекторе.
369			anSetData(list, 'layer-list_id', list_id);
370			anSetData(list, 'layer-inited', true);
371
372			return list_id;
373		}
374
375		return anData(list, 'layer-list_id');
376	}
377	
378	this.productList = function($list){
379		var list = anEl($list);
380		var list_id = this.productListInit(list),
381			products = [];
382
383		if(!list) return;
384
385		Array.prototype.forEach.call(list.querySelectorAll(this.selectors.product), function(product_node){
386			if(!anData(product_node, 'layer-inited')){
387				var product = self.initProductData(product_node);
388
389				self.products[list_id][product.id] = product;
390				products.push(product);
391
392				anSetData(product_node, 'layer-inited', true);
393			}
394		});
395
396		this.productImpressions(products);
397	}
398
399	this.viewBasket = function(products){
400		if ($$.ga_ecomm) {
401			$$.ga_ecomm.viewBasket(products);
402		}
403	}
404
405	this.viewOrder = function(products){
406		if ($$.ga_ecomm) {
407			$$.ga_ecomm.viewOrder(products);
408		}
409	}
410}
411
412$$._vk_ecomm = function(){
413	var PIXEL_ID = 3306496
414	var self = this;
415	var bufferPushData = [];
416	
417	this.inited = function() {
418		var inited = window._tmr && window._tmr.push;
419		if (!inited) {
420			return false;
421		}
422
423		self.restore();
424
425		return true;
426	}
427
428	this.restore = function() {
429		if (bufferPushData.length) {
430			console.log('vk restore push', pushData);
431			for (let index = 0; index < bufferPushData.length; index++) {
432				_tmr.push(bufferPushData[index]);
433			}
434			bufferPushData = [];
435		}
436	}
437
438	this.push = function(pushData) {
439		if (!self.inited()) {
440			console.log('vk save push', pushData);
441			bufferPushData.push(pushData);
442		}
443		else {
444			console.log('vk push', pushData);
445			_tmr.push(pushData);
446		}
447	}
448
449	this.viewProduct = function(product) {
450		console.log('vk viewProduct', product);
451		this.push({
452			type: "reachGoal",
453			id: PIXEL_ID,
454			value: ''+product.price,
455			goal: "viewProduct",
456			params: {
457				product_id: ''+product.id}
458			}
459		);
460	}
461
462	this.addBasketProduct = function(products) {
463		console.log('vk addBasketProduct', products);
464		for (var index = 0; index < products.length; index++) {
465			var product = products[index];
466			self.push({
467				type: "reachGoal",
468				id: PIXEL_ID,
469				value: ''+product.price,
470				goal: "addToCart",
471				params: {
472					product_id: ''+product.id
473				}
474			});
475		}
476	}
477
478	this.removeBasketProduct = function(products) {
479		// console.log('vk removeBasketProduct', products);
480	}
481
482	this.purchaseOrder = function(orderId, products) {
483		console.log('vk purchaseOrder', orderId, products);
484		var product_ids = [];
485		var sum = 0;
486		for (var index = 0; index < products.length; index++) {
487			var product = products[index];
488			// Конвертирование к строке
489			product_ids.push('' + product.id);
490			// Конвертирование к числу
491			sum += product.price * 1 * product.quantity * 1;
492		}
493		self.push({
494			type: "reachGoal",
495			id: PIXEL_ID,
496			value: '' + sum,
497			goal: "purchase",
498			params: {
499				product_id: product_ids
500			}
501		});
502	}
503}
504
505$$._ga_ecomm = function(){
506	var self = this;
507	var bufferPushData = [];
508	
509	this.inited = function() {
510		var inited = window.dataLayer && window.dataLayer.push;
511		if (!inited) {
512			return false;
513		}
514
515		self.restore();
516
517		return true;
518	}
519
520	this.restore = function() {
521		if (bufferPushData.length) {
522			// console.log('ga restore push', pushData);
523			for (let index = 0; index < bufferPushData.length; index++) {
524				dataLayer.push(bufferPushData[index]);
525			}
526			bufferPushData = [];
527		}
528	}
529
530	this.push = function(pushData) {
531		if (!self.inited()) {
532			// console.log('ga save push', pushData);
533			bufferPushData.push(pushData);
534		}
535		else {
536			console.log('ga push', pushData);
537			dataLayer.push(pushData);
538		}
539	}
540
541	this.processProduct = function(product) {
542		product.categories = product.category ? product.category.split(/\s*\/\s*/) : '';
543		var result = {
544			'item_name': product.name, // Название товара, карточку которого пользователь просматривает
545			'item_id': product.id, // Идентификатор товара
546			'price': product.price, // Цена 1 единицы товара
547			'item_brand': product.brand, // Бренд товара
548			'quantity': product.quantity, // Параметр, в котором фиксируется количество товара
549			'item_variant': product.variant, // Вариант товара, карточку которого просматривает пользователь
550			// 'discount': '100',  //	Размер скидки на товар, карточку которого просматривает пользователь
551		};
552		for (let index = 0; index < product.categories.length; index++) {
553			const prefix = index ? ''+(index+1) : '';
554			result['item_category'+prefix] = product.categories[index];
555		}
556		return result;
557	}
558
559	this.processProducts = function(products) {
560		var result = [];
561		for (let index = 0; index < products.length; index++) {
562			let product = this.processProduct(products[index]);
563			product['index'] = index + 1;
564			result[index] = product;
565		}
566		return result;
567	}
568
569	this.viewProducts = function(products) {
570		// console.log('ga viewProducts', products);
571		this.push({
572			'event': 'view_item_list',
573			'ecommerce': {
574				'items': this.processProducts(products)
575			}
576		});
577	}
578
579	this.viewProduct = function(product) {
580		// console.log('ga viewProduct', product);
581		this.push({
582			'event': 'view_item',
583			'ecommerce': {
584				'items': [this.processProduct(product)]
585			}
586		});
587	}
588
589	this.addBasketProduct = function(products) {
590		// console.log('ga addBasketProduct', products);
591		this.push({
592			'event': 'add_to_cart',
593			'ecommerce': {
594				'items': this.processProducts(products)
595			}
596		});
597	}
598
599	this.removeBasketProduct = function(products) {
600		// console.log('ga removeBasketProduct', products);
601		this.push({
602			'event': 'remove_from_cart',
603			'ecommerce': {
604				'items': this.processProducts(products)
605			}
606		});
607	}
608
609	this.viewBasket = function(products) {
610		// console.log('ga viewBasket', products);
611		this.push({
612			'event': 'view_cart',
613			'ecommerce': {
614				'items': this.processProducts(products)
615			}
616		});
617	}
618
619	this.viewOrder = function(products) {
620		// console.log('ga viewOrder', products);
621		this.push({
622			'event': 'begin_checkout',
623			'ecommerce': {
624				'items': this.processProducts(products)
625			}
626		});
627		sendYaReachGoal('klik-po-knopke-oformit-zakaz-korzina');
628	}
629
630	this.purchaseOrder = function(orderId, sum, products) {
631		// console.log('ga purchaseOrder', orderId, products);
632		this.push({
633			'event': 'purchase',
634			'ecommerce': {
635				'transaction_id': orderId, // Идентификатор транзакции
636				'value': sum, // Общая сумма корзины на момент отправки события
637				'currency': 'RUB',
638				'shipping': 0, // Стоимость доставки
639				'items': this.processProducts(products)
640			}
641		});
642	}
643}
644
645
646function anReady(fn) {
647	if (document.readyState === 'loading') {
648		document.addEventListener('DOMContentLoaded', fn);
649	} else {
650		fn();
651	}
652}
653
654anReady(function(){
655	// Реальные объекты аналитики перетирают заглушки из commonCatalog_new.js.
656	$$.metrika = new $$._metrika();
657	$$.vk_ecomm = new $$._vk_ecomm();
658	$$.ga_ecomm = new $$._ga_ecomm();
659
660	if (window.basketMetrika && basketMetrika.length) {
661		$$.metrika.viewBasket(basketMetrika);
662	}
663});

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.