modified: main_dc/docker-compose.yml
modified: main_dc/nginx/nginx-ssl.conf new file: main_dc/valitovgaziz/analytics/Dockerfile new file: main_dc/valitovgaziz/analytics/package.json new file: main_dc/valitovgaziz/analytics/server.js new file: main_dc/valitovgaziz/html/JavaScript/analytics.js modified: main_dc/valitovgaziz/html/index.html add nginx settings for api logs for valitovgaziz.ru site, add container for metrica container, add metrica scripts on site valitovgaziz.ru
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
certbot:
|
certbot:
|
||||||
build:
|
build:
|
||||||
context: ./certbot
|
context: ./certbot
|
||||||
@@ -33,6 +34,7 @@ services:
|
|||||||
- ./yalarba/serv_spa/spa/vue/dist:/usr/share/nginx/yalarba/html
|
- ./yalarba/serv_spa/spa/vue/dist:/usr/share/nginx/yalarba/html
|
||||||
- ./valitovgaziz/html:/usr/share/nginx/valitovgaziz/html
|
- ./valitovgaziz/html:/usr/share/nginx/valitovgaziz/html
|
||||||
- ./BB/bbvue/dist:/usr/share/nginx/begushiybashkir/html
|
- ./BB/bbvue/dist:/usr/share/nginx/begushiybashkir/html
|
||||||
|
- analytics_logs:/var/log/analytics:ro
|
||||||
networks:
|
networks:
|
||||||
- web-network
|
- web-network
|
||||||
- internal
|
- internal
|
||||||
@@ -42,6 +44,33 @@ services:
|
|||||||
- certbot
|
- certbot
|
||||||
- api
|
- api
|
||||||
- api_bb
|
- api_bb
|
||||||
|
- analytics
|
||||||
|
|
||||||
|
analytics:
|
||||||
|
build:
|
||||||
|
context: ./analytics
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: analytics
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "9999:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=3000
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
- LOG_RETENTION_DAYS=30
|
||||||
|
volumes:
|
||||||
|
- analytics_logs:/app/logs
|
||||||
|
- analytics_data:/app/data
|
||||||
|
networks:
|
||||||
|
- web-network
|
||||||
|
- internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
api:
|
api:
|
||||||
build:
|
build:
|
||||||
@@ -197,7 +226,15 @@ services:
|
|||||||
- app-network
|
- app-network
|
||||||
- web-network
|
- web-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8081/health"]
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"wget",
|
||||||
|
"--no-verbose",
|
||||||
|
"--tries=1",
|
||||||
|
"--spider",
|
||||||
|
"http://localhost:8081/health",
|
||||||
|
]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
@@ -208,6 +245,8 @@ volumes:
|
|||||||
db_tp_data:
|
db_tp_data:
|
||||||
db_bb_data:
|
db_bb_data:
|
||||||
api_bb_uploads:
|
api_bb_uploads:
|
||||||
|
analytics_logs: # Volume для логов аналитики
|
||||||
|
analytics_data: # Volume для данных аналитики
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
web-network:
|
web-network:
|
||||||
|
|||||||
@@ -76,6 +76,23 @@ server {
|
|||||||
index index.html;
|
index index.html;
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /api/analytics {
|
||||||
|
proxy_pass http://analytics:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# CORS headers
|
||||||
|
add_header Access-Control-Allow-Origin "*" always;
|
||||||
|
add_header Access-Control-Allow-Methods "POST, OPTIONS" always;
|
||||||
|
add_header Access-Control-Allow-Headers "Content-Type" always;
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем package.json и устанавливаем зависимости
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Копируем исходный код
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Создаем директории для логов
|
||||||
|
RUN mkdir -p /app/logs /app/data
|
||||||
|
|
||||||
|
# Создаем непривилегированного пользователя
|
||||||
|
RUN addgroup -g 1001 -S nodejs
|
||||||
|
RUN adduser -S analytics -u 1001
|
||||||
|
|
||||||
|
# Меняем владельца файлов
|
||||||
|
RUN chown -R analytics:nodejs /app
|
||||||
|
|
||||||
|
USER analytics
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "analytics-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Custom analytics server for tracking website events",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "nodemon server.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"helmet": "^7.0.0",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
|
"compression": "^1.7.4",
|
||||||
|
"rate-limiter-flexible": "^3.0.8",
|
||||||
|
"winston": "^3.10.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.0.1"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"analytics",
|
||||||
|
"tracking",
|
||||||
|
"express"
|
||||||
|
],
|
||||||
|
"author": "Valitov Gaziz",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const compression = require('compression');
|
||||||
|
const morgan = require('morgan');
|
||||||
|
const { RateLimiterMemory } = require('rate-limiter-flexible');
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Настройка лимитера запросов
|
||||||
|
const rateLimiter = new RateLimiterMemory({
|
||||||
|
keyGenerator: (req) => req.ip,
|
||||||
|
points: 100, // 100 запросов
|
||||||
|
duration: 60, // за 60 секунд
|
||||||
|
});
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(helmet({
|
||||||
|
crossOriginResourcePolicy: { policy: "cross-origin" }
|
||||||
|
}));
|
||||||
|
app.use(compression());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json({ limit: '1mb' }));
|
||||||
|
|
||||||
|
// Логирование запросов
|
||||||
|
const accessLogStream = require('fs').createWriteStream(
|
||||||
|
path.join(__dirname, 'logs', 'access.log'),
|
||||||
|
{ flags: 'a' }
|
||||||
|
);
|
||||||
|
app.use(morgan('combined', { stream: accessLogStream }));
|
||||||
|
|
||||||
|
// Winston логгер для приложения
|
||||||
|
const winston = require('winston');
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
level: process.env.LOG_LEVEL || 'info',
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.timestamp(),
|
||||||
|
winston.format.json()
|
||||||
|
),
|
||||||
|
transports: [
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: path.join(__dirname, 'logs', 'error.log'),
|
||||||
|
level: 'error'
|
||||||
|
}),
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: path.join(__dirname, 'logs', 'combined.log')
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
logger.add(new winston.transports.Console({
|
||||||
|
format: winston.format.simple()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware для ограничения запросов
|
||||||
|
app.use(async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
await rateLimiter.consume(req.ip);
|
||||||
|
next();
|
||||||
|
} catch (rejRes) {
|
||||||
|
res.status(429).json({
|
||||||
|
error: 'Too Many Requests',
|
||||||
|
retryAfter: Math.ceil(rejRes.msBeforeNext / 1000)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функция для записи аналитики в файл
|
||||||
|
async function writeAnalytics(data) {
|
||||||
|
try {
|
||||||
|
const timestamp = new Date().toISOString().split('T')[0];
|
||||||
|
const logFile = path.join(__dirname, 'data', `analytics-${timestamp}.json`);
|
||||||
|
|
||||||
|
const logEntry = {
|
||||||
|
receivedAt: new Date().toISOString(),
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
|
||||||
|
// Читаем существующие данные
|
||||||
|
let existingData = [];
|
||||||
|
try {
|
||||||
|
const fileContent = await fs.readFile(logFile, 'utf8');
|
||||||
|
existingData = JSON.parse(fileContent);
|
||||||
|
} catch (error) {
|
||||||
|
// Файл не существует или пустой - это нормально
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем новую запись
|
||||||
|
existingData.push(logEntry);
|
||||||
|
|
||||||
|
// Записываем обратно
|
||||||
|
await fs.writeFile(logFile, JSON.stringify(existingData, null, 2));
|
||||||
|
|
||||||
|
logger.info('Analytics event saved', {
|
||||||
|
eventsCount: data.events?.length || 1,
|
||||||
|
sessionId: data.sessionId
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error writing analytics data', { error: error.message });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Роуты
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: 'OK',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
service: 'analytics-server'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/analytics', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { events, sessionId } = req.body;
|
||||||
|
|
||||||
|
if (!events || !Array.isArray(events)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid data format: events array required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeAnalytics({ events, sessionId });
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
received: events.length,
|
||||||
|
sessionId
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Analytics processing error', { error: error.message });
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Internal server error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Статистика (только для разработки)
|
||||||
|
app.get('/api/stats', async (req, res) => {
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
return res.status(403).json({ error: 'Forbidden' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dataDir = path.join(__dirname, 'data');
|
||||||
|
const files = await fs.readdir(dataDir);
|
||||||
|
const analyticsFiles = files.filter(f => f.startsWith('analytics-'));
|
||||||
|
|
||||||
|
let totalEvents = 0;
|
||||||
|
const sessions = new Set();
|
||||||
|
|
||||||
|
for (const file of analyticsFiles.slice(-7)) { // Последние 7 дней
|
||||||
|
const content = await fs.readFile(path.join(dataDir, file), 'utf8');
|
||||||
|
const data = JSON.parse(content);
|
||||||
|
|
||||||
|
data.forEach(entry => {
|
||||||
|
totalEvents += entry.events?.length || 0;
|
||||||
|
if (entry.sessionId) sessions.add(entry.sessionId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
totalEvents,
|
||||||
|
uniqueSessions: sessions.size,
|
||||||
|
filesCount: analyticsFiles.length
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработка 404
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).json({ error: 'Route not found' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработка ошибок
|
||||||
|
app.use((error, req, res, next) => {
|
||||||
|
logger.error('Unhandled error', { error: error.message });
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
|
logger.info(`Analytics server running on port ${PORT}`);
|
||||||
|
console.log(`🔍 Analytics server: http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
// analytics.js - собственный счетчик аналитики для браузера
|
||||||
|
class CustomAnalytics {
|
||||||
|
constructor() {
|
||||||
|
this.endpoint = 'https://valitovgaziz.ru/api/analytics'; // Ваш endpoint для сбора данных
|
||||||
|
this.queue = [];
|
||||||
|
this.isOnline = navigator.onLine;
|
||||||
|
this.sessionId = this.getSessionId();
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Загружаем сохраненные данные из localStorage
|
||||||
|
this.loadFromStorage();
|
||||||
|
|
||||||
|
// Отслеживание событий
|
||||||
|
this.trackPageView();
|
||||||
|
this.setupEventListeners();
|
||||||
|
|
||||||
|
// Периодическая отправка данных
|
||||||
|
setInterval(() => this.flushQueue(), 30000);
|
||||||
|
|
||||||
|
// Отслеживание онлайн/офлайн статуса
|
||||||
|
window.addEventListener('online', () => {
|
||||||
|
this.isOnline = true;
|
||||||
|
this.flushQueue();
|
||||||
|
});
|
||||||
|
window.addEventListener('offline', () => {
|
||||||
|
this.isOnline = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отправка данных перед закрытием страницы
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
this.trackEvent('page', 'unload');
|
||||||
|
this.flushQueueSync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
trackPageView() {
|
||||||
|
const data = {
|
||||||
|
type: 'pageview',
|
||||||
|
url: window.location.href,
|
||||||
|
referrer: document.referrer,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
screen: `${screen.width}x${screen.height}`,
|
||||||
|
language: navigator.language,
|
||||||
|
sessionId: this.sessionId
|
||||||
|
};
|
||||||
|
this.addToQueue(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackEvent(category, action, label = null, value = null) {
|
||||||
|
const data = {
|
||||||
|
type: 'event',
|
||||||
|
category,
|
||||||
|
action,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
url: window.location.href,
|
||||||
|
sessionId: this.sessionId
|
||||||
|
};
|
||||||
|
this.addToQueue(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackClick(element, context = 'unknown') {
|
||||||
|
const data = {
|
||||||
|
type: 'click',
|
||||||
|
element: element.tagName,
|
||||||
|
text: element.textContent?.substring(0, 100),
|
||||||
|
context,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
url: window.location.href,
|
||||||
|
sessionId: this.sessionId
|
||||||
|
};
|
||||||
|
this.addToQueue(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
addToQueue(data) {
|
||||||
|
this.queue.push(data);
|
||||||
|
|
||||||
|
// Сохраняем в localStorage
|
||||||
|
this.saveToStorage();
|
||||||
|
|
||||||
|
// Отправляем сразу если онлайн и очередь большая
|
||||||
|
if (this.isOnline && this.queue.length >= 3) {
|
||||||
|
this.flushQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async flushQueue() {
|
||||||
|
if (!this.isOnline || this.queue.length === 0) return;
|
||||||
|
|
||||||
|
const batch = [...this.queue];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
events: batch,
|
||||||
|
sessionId: this.sessionId
|
||||||
|
}),
|
||||||
|
keepalive: true // Позволяет отправлять данные даже при закрытии страницы
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Удаляем отправленные данные из очереди
|
||||||
|
this.queue = this.queue.filter(item => !batch.includes(item));
|
||||||
|
this.saveToStorage();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Analytics offline, storing locally');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flushQueueSync() {
|
||||||
|
if (this.queue.length === 0) return;
|
||||||
|
|
||||||
|
// Используем sendBeacon для надежной отправки при закрытии страницы
|
||||||
|
const data = JSON.stringify({
|
||||||
|
events: this.queue,
|
||||||
|
sessionId: this.sessionId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (navigator.sendBeacon) {
|
||||||
|
navigator.sendBeacon(this.endpoint, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSessionId() {
|
||||||
|
let sessionId = localStorage.getItem('ga_session_id');
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (!sessionId) {
|
||||||
|
sessionId = 'sess_' + now + '_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
localStorage.setItem('ga_session_id', sessionId);
|
||||||
|
localStorage.setItem('ga_session_start', now);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем время последней активности
|
||||||
|
localStorage.setItem('ga_last_activity', now);
|
||||||
|
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveToStorage() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('ga_queue', JSON.stringify(this.queue));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Cannot save analytics to localStorage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFromStorage() {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem('ga_queue');
|
||||||
|
if (stored) {
|
||||||
|
const parsed = JSON.parse(stored);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
this.queue = parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Cannot load analytics from localStorage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Отслеживание кликов по кнопкам
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (e.target.matches('button, .btn, a[href]')) {
|
||||||
|
const context = e.target.closest('.section') ?
|
||||||
|
e.target.closest('.section').querySelector('h2')?.textContent || 'unknown' :
|
||||||
|
'global';
|
||||||
|
this.trackClick(e.target, context);
|
||||||
|
|
||||||
|
// Специальные события для кнопок сотрудничества
|
||||||
|
if (e.target.textContent.includes('сотрудничество') || e.target.textContent.includes('Написать')) {
|
||||||
|
this.trackEvent('conversion', 'contact_click', e.target.textContent.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отслеживание отправки форм
|
||||||
|
document.addEventListener('submit', (e) => {
|
||||||
|
this.trackEvent('form', 'submit', e.target.id || 'unknown');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отслеживание видимости секций
|
||||||
|
this.setupSectionTracking();
|
||||||
|
|
||||||
|
// Отслеживание внешних ссылок
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const link = e.target.closest('a[href]');
|
||||||
|
if (link && link.hostname !== window.location.hostname) {
|
||||||
|
this.trackEvent('outbound', 'click', link.href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupSectionTracking() {
|
||||||
|
const sections = document.querySelectorAll('.section');
|
||||||
|
const observedSections = new Set();
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
|
||||||
|
const sectionId = entry.target.id ||
|
||||||
|
entry.target.querySelector('h2')?.textContent?.substring(0, 50) ||
|
||||||
|
'unknown_section';
|
||||||
|
|
||||||
|
if (!observedSections.has(sectionId)) {
|
||||||
|
observedSections.add(sectionId);
|
||||||
|
this.trackEvent('content', 'section_view', sectionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
threshold: [0.5],
|
||||||
|
rootMargin: '0px 0px -10% 0px'
|
||||||
|
});
|
||||||
|
|
||||||
|
sections.forEach(section => {
|
||||||
|
observer.observe(section);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация при полной загрузке DOM
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.analytics = new CustomAnalytics();
|
||||||
|
|
||||||
|
// Глобальные функции для ручного отслеживания
|
||||||
|
window.trackEvent = (category, action, label, value) => {
|
||||||
|
if (window.analytics) {
|
||||||
|
window.analytics.trackEvent(category, action, label, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Отслеживание специальных событий для вашего сайта
|
||||||
|
const specialButtons = document.querySelectorAll('[onclick*="sendMessageTelegram"]');
|
||||||
|
specialButtons.forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
trackEvent('business', 'telegram_click', btn.textContent.trim());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отслеживание просмотра ключевых элементов
|
||||||
|
const keyElements = document.querySelectorAll('.hero, .team-section, .yalarba-section');
|
||||||
|
const elementObserver = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const elementType = entry.target.className.split(' ')[0];
|
||||||
|
trackEvent('engagement', `${elementType}_viewed`);
|
||||||
|
elementObserver.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { threshold: 0.3 });
|
||||||
|
|
||||||
|
keyElements.forEach(el => elementObserver.observe(el));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fallback для старых браузеров
|
||||||
|
if (!window.Promise) {
|
||||||
|
console.warn('Custom analytics requires Promise support');
|
||||||
|
window.trackEvent = function () { };
|
||||||
|
}
|
||||||
@@ -15,53 +15,10 @@
|
|||||||
<script src="scripts.js"></script>
|
<script src="scripts.js"></script>
|
||||||
<script src="darkThemeToggle.js"></script>
|
<script src="darkThemeToggle.js"></script>
|
||||||
<script src="digital_background.js"></script>
|
<script src="digital_background.js"></script>
|
||||||
|
<script src="JavaScript/analytics.js"></script>
|
||||||
<title>ValitovGaziz - Предприниматель - Fullstack-разработчик</title>
|
<title>ValitovGaziz - Предприниматель - Fullstack-разработчик</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Yandex.Metrika counter -->
|
|
||||||
<script type="text/javascript">
|
|
||||||
(function (m, e, t, r, i, k, a) {
|
|
||||||
m[i] =
|
|
||||||
m[i] ||
|
|
||||||
function () {
|
|
||||||
(m[i].a = m[i].a || []).push(arguments);
|
|
||||||
};
|
|
||||||
m[i].l = 1 * new Date();
|
|
||||||
for (var j = 0; j < document.scripts.length; j++) {
|
|
||||||
if (document.scripts[j].src === r) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(k = e.createElement(t)),
|
|
||||||
(a = e.getElementsByTagName(t)[0]),
|
|
||||||
(k.async = 1),
|
|
||||||
(k.src = r),
|
|
||||||
a.parentNode.insertBefore(k, a);
|
|
||||||
})(
|
|
||||||
window,
|
|
||||||
document,
|
|
||||||
"script",
|
|
||||||
"https://mc.yandex.ru/metrika/tag.js",
|
|
||||||
"ym"
|
|
||||||
);
|
|
||||||
|
|
||||||
ym(103321468, "init", {
|
|
||||||
clickmap: true,
|
|
||||||
trackLinks: true,
|
|
||||||
accurateTrackBounce: true,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<noscript
|
|
||||||
><div>
|
|
||||||
<img
|
|
||||||
src="https://mc.yandex.ru/watch/103321468"
|
|
||||||
style="position: absolute; left: -9999px"
|
|
||||||
alt=""
|
|
||||||
/></div
|
|
||||||
></noscript>
|
|
||||||
<!-- /Yandex.Metrika counter -->
|
|
||||||
|
|
||||||
<header class="hero">
|
<header class="hero">
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<div class="hero-text">
|
<div class="hero-text">
|
||||||
|
|||||||
Reference in New Issue
Block a user