Передісторія
Стаття виникла сама по собі оскільки була цікава ідея – якось трекати взаємодію юзерів з PDF-документами на сайті. Компанії часто використовують PDF-ки. Вайтпейпери, інструкції, звіти, прайси, шаблони… Як власнику сайту знати, що документ переглядали? Максимум що може знати власник сайту, скільки раз відкривали по кнопці. А це не багато. Може щось там дивився… а що саме? Чи дійшов до кінця документу? А чи скачав він його? Подивився хоч півсторінки? А може він його кинув на друк?
Якщо піти з цим питанням до ChatGPT він скаже, шо можна і напише спосіб, як відстежити відкривання PDF, або запропонує iframe. Спойрел: він також не допоможе :). Читачі мого блогу напевно таке вміють зробити і без ChatGPT.
Будь-яка веб-аналітика цього не вміє, бо не може трекати взаємодію з PDF. Цей файл не є трекабельним.
Тому я пішов шукати глибше і згадав, що ще колись в 201Х, я вчив HTML5 і там був малозрозумілий і малодоступний тег <canvas>. Бібліотек мінімум було, писати шось з нуля під нього – треба бути профіком. А для мене верстка була не чимось профільним.
То ж мої пошуки розпочалися саме з інвестігейту “а шо нового є під canvas” він ж може показувати все що завгодно і все там можна трекати. І тут я натрапив на PDF.JS. Бібліотека від Mozilla, яка може закрити дане питання дуже легко, безкоштовно і головне краще ніж будь-який платний сервіс. Бо івентами можна завішати весь документ. Хіба не скл
А тепер трохи практики
Етап 1: Встановлення PDF.js
Хостимо свій pdf.js + pdf.worker.js та web/viewer.html в одному каталозі. Наприклад:
/pdfjs/
├── build/
│ ├── pdf.js
│ └── pdf.worker.js
└── web/
└── viewer.htmlІ викликаємо у сторінці (через iframe):
<iframe src="/pdfjs/web/viewer.html?file=/docs/sample.pdf"
width="100%" height="600"></iframe>Етап 2: Події з eventBus
З PDF.js v2.x можна підписатись на колбеки з документації:
document.addEventListener('webviewerloaded', () => {
PDFViewerApplication.initializedPromise.then(() => {
const bus = PDFViewerApplication.eventBus;
bus.on("documentloaded", () => console.log('PDF завантажений'));
bus.on("pagechanging", e => console.log('Зміна сторінки', e.pageNumber));
bus.on("scalechanging", e => console.log('Масштаб', e.scale));
});
});І ще DOM‑івент:
document.addEventListener('pagechange', e =>
console.log('DOM event pagechange', e.pageNumber, e.previousPageNumber)
);
Етап 3: Прив’язка треків до івентів
function sendPDFEvent(name, data = {}) {
gtag('event', name, { event_category: 'PDF', ...data });
}
// коли завантажили PDF
bus.on("documentloaded", () =>
sendPDFEvent('PDF Loaded', { pages: PDFViewerApplication.pdfDocument.numPages })
);
// коли переходять сторінку
bus.on("pagechanging", e =>
sendPDFEvent('PDF Page View', { page: e.pageNumber })
);
// масштаб змінюють
bus.on("scalechanging", e =>
sendPDFEvent('PDF Scale Change', { scale: e.scale })
);
Етап 4: Треба більше даних? Ось вам і інші тригери
- Скачування і друк: в
viewer.htmlкнопки мають id#download,#print:
document.getElementById('download')
.addEventListener('click', () => sendPDFEvent('PDF Download'));
document.getElementById('print')
.addEventListener('click', () => sendPDFEvent('PDF Print'));
- Глибина скролу: PDF.js не дає події, тому юзаємо простий скрол-трекінг:
const vc = document.getElementById('viewerContainer');
let flags = {};
vc.addEventListener('scroll', () => {
const pct = Math.floor((vc.scrollTop + vc.clientHeight) / vc.scrollHeight * 100);
[25,50,75,100].forEach(th => {
if (pct >= th && !flags[th]) {
flags[th] = true;
sendPDFEvent('PDF Scroll Depth', { depth: th + '%' });
}
});
});
- Час взаємодії: додаємо
setInterval, корисне: якщо ніяких подій немає 5 секунд, вважаємо, що відмова.
Ось що вийшло в мене:
Skip to PDF contentЯкщо не підходить iframe
Представимо, що PDF не має відображатися на цій самій сторінці, а має жити окремо за своїм посиланням. Тобто як стандартний файл PDF десь розміщений на сервері і його можна скачати. Ось гіперпосилання на PDF. В такому випадку ось порядок дій:
Крок 1: Додаємо GTM-код у viewer.html
- Відкрий файл
pdfjs/web/viewer.html - Знайди тег
<head>і встав всередину свій GTM Snippet:
<!-- Google Tag Manager -->
<script>
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX'); // <== сюди встав свій GTM ID
</script>
<!-- End Google Tag Manager -->- Потім відразу після відкриття
<body>встав свій noscript iframe:
<!-- Google Tag Manager (noscript) --> <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript> <!-- End Google Tag Manager (noscript) -->
Крок 2: Налаштуй GTM для трекінгу
1. Трекінг кліків по кнопках
Приклад для кнопки «Download»:
- Створюємо тригер:
Тип: Click – All Elements
Фільтр:Click ID→equals→download - Створимо тег GA4 Event:
- Назва івенту:
pdf_download - Категорія:
PDF - Прив’язка: до тригера
Click on Download
- Назва івенту:
Аналогічно налаштуємо трекінг для:
| Подія | Click ID |
|---|---|
print | |
| Previous Page | previous |
| Next Page | next |
2. Трекінг перегляду сторінок через Custom Event
PDF.js кидає DOM подію pagechange. Можна використати:
- Створимо тригер типу:
Custom Event- Назва події:
pagechange
- Назва події:
- У цьому тригері можна додати змінну Page Number:
- Створимо нову User-Defined Variable
- Тип: DOM Element
- Selection Method:
CSS Selector - Selector:
#pageNumber(або#pageNumberInput) - Attribute name:
value
- Прив’язуємо цю змінну до тегу GA4, і в параметри передаємр її як
page_number
3. Що ще зробити
- Встановити таймер (Trigger type: Timer) для відстеження часу перегляду
- Відслідковувати кількість інших взаємодій
- Створити Scroll Depth trigger на
#viewerContainer(але потрібно буде трохи JavaScript-Variables)
Бонус для лінивих – готовий JSON для імпорту в GTM (хто вміє) =)
{
"exportFormatVersion": 2,
"exportTime": "2025-06-13T12:00:00",
"containerVersion": {
"container": {
"publicId": "GTM-XXXXXXX",
"name": "PDF.js Tracker"
},
"tag": [
{
"name": "GA4 - PDF Download",
"type": "ga4Event",
"parameter": [
{
"key": "eventName",
"value": "pdf_download"
},
{
"key": "eventParameters",
"type": "list",
"list": [
{
"type": "map",
"map": [
{
"key": "name",
"value": "event_category"
},
{
"key": "value",
"value": "PDF"
}
]
}
]
}
],
"triggerId": [
"1"
]
},
{
"name": "GA4 - PDF Print",
"type": "ga4Event",
"parameter": [
{
"key": "eventName",
"value": "pdf_print"
},
{
"key": "eventParameters",
"type": "list",
"list": [
{
"type": "map",
"map": [
{
"key": "name",
"value": "event_category"
},
{
"key": "value",
"value": "PDF"
}
]
}
]
}
],
"triggerId": [
"2"
]
},
{
"name": "GA4 - PDF Page View",
"type": "ga4Event",
"parameter": [
{
"key": "eventName",
"value": "pdf_page_view"
},
{
"key": "eventParameters",
"type": "list",
"list": [
{
"type": "map",
"map": [
{
"key": "name",
"value": "page_number"
},
{
"key": "value",
"value": "{{PDF Page Number}}"
}
]
}
]
}
],
"triggerId": [
"3"
]
}
],
"trigger": [
{
"name": "Click - PDF Download",
"type": "CLICK",
"filter": [
{
"type": "EQUALS",
"parameter": [
{
"type": "template",
"key": "arg0",
"value": "{{Click ID}}"
},
{
"type": "template",
"key": "arg1",
"value": "download"
}
]
}
],
"uniqueTriggerId": "1"
},
{
"name": "Click - PDF Print",
"type": "CLICK",
"filter": [
{
"type": "EQUALS",
"parameter": [
{
"type": "template",
"key": "arg0",
"value": "{{Click ID}}"
},
{
"type": "template",
"key": "arg1",
"value": "print"
}
]
}
],
"uniqueTriggerId": "2"
},
{
"name": "Event - PDF Page Change",
"type": "CUSTOM_EVENT",
"customEventFilter": [
{
"type": "EQUALS",
"parameter": [
{
"type": "template",
"key": "arg0",
"value": "{{_event}}"
},
{
"type": "template",
"key": "arg1",
"value": "pagechange"
}
]
}
],
"uniqueTriggerId": "3"
}
],
"variable": [
{
"name": "PDF Page Number",
"type": "DOM_ELEMENT",
"parameter": [
{
"key": "attributeName",
"value": "value"
},
{
"key": "elementSelector",
"value": "#pageNumber"
},
{
"key": "selectorMethod",
"value": "CSS_SELECTOR"
}
]
}
]
}
}