From a013fcacd8d00b837763ac66fd0d7d62595defa0 Mon Sep 17 00:00:00 2001 From: valitovgaziz Date: Tue, 11 Nov 2025 03:22:14 +0500 Subject: [PATCH] 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 --- main_dc/docker-compose.yml | 41 ++- main_dc/nginx/nginx-ssl.conf | 17 ++ main_dc/valitovgaziz/analytics/Dockerfile | 26 ++ main_dc/valitovgaziz/analytics/package.json | 30 ++ main_dc/valitovgaziz/analytics/server.js | 192 +++++++++++++ .../valitovgaziz/html/JavaScript/analytics.js | 270 ++++++++++++++++++ main_dc/valitovgaziz/html/index.html | 45 +-- 7 files changed, 576 insertions(+), 45 deletions(-) create mode 100644 main_dc/valitovgaziz/analytics/Dockerfile create mode 100644 main_dc/valitovgaziz/analytics/package.json create mode 100644 main_dc/valitovgaziz/analytics/server.js create mode 100644 main_dc/valitovgaziz/html/JavaScript/analytics.js diff --git a/main_dc/docker-compose.yml b/main_dc/docker-compose.yml index 9ab1f5d..0ffb79e 100644 --- a/main_dc/docker-compose.yml +++ b/main_dc/docker-compose.yml @@ -1,4 +1,5 @@ services: + certbot: build: context: ./certbot @@ -33,6 +34,7 @@ services: - ./yalarba/serv_spa/spa/vue/dist:/usr/share/nginx/yalarba/html - ./valitovgaziz/html:/usr/share/nginx/valitovgaziz/html - ./BB/bbvue/dist:/usr/share/nginx/begushiybashkir/html + - analytics_logs:/var/log/analytics:ro networks: - web-network - internal @@ -42,6 +44,33 @@ services: - certbot - api - 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: build: @@ -197,7 +226,15 @@ services: - app-network - web-network 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 timeout: 10s retries: 3 @@ -208,6 +245,8 @@ volumes: db_tp_data: db_bb_data: api_bb_uploads: + analytics_logs: # Volume для логов аналитики + analytics_data: # Volume для данных аналитики networks: web-network: diff --git a/main_dc/nginx/nginx-ssl.conf b/main_dc/nginx/nginx-ssl.conf index b45b75d..353cdb1 100644 --- a/main_dc/nginx/nginx-ssl.conf +++ b/main_dc/nginx/nginx-ssl.conf @@ -76,6 +76,23 @@ server { index 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 { diff --git a/main_dc/valitovgaziz/analytics/Dockerfile b/main_dc/valitovgaziz/analytics/Dockerfile new file mode 100644 index 0000000..65087c7 --- /dev/null +++ b/main_dc/valitovgaziz/analytics/Dockerfile @@ -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"] \ No newline at end of file diff --git a/main_dc/valitovgaziz/analytics/package.json b/main_dc/valitovgaziz/analytics/package.json new file mode 100644 index 0000000..228fbd0 --- /dev/null +++ b/main_dc/valitovgaziz/analytics/package.json @@ -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" +} \ No newline at end of file diff --git a/main_dc/valitovgaziz/analytics/server.js b/main_dc/valitovgaziz/analytics/server.js new file mode 100644 index 0000000..7951673 --- /dev/null +++ b/main_dc/valitovgaziz/analytics/server.js @@ -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}`); +}); \ No newline at end of file diff --git a/main_dc/valitovgaziz/html/JavaScript/analytics.js b/main_dc/valitovgaziz/html/JavaScript/analytics.js new file mode 100644 index 0000000..073aeaa --- /dev/null +++ b/main_dc/valitovgaziz/html/JavaScript/analytics.js @@ -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 () { }; +} \ No newline at end of file diff --git a/main_dc/valitovgaziz/html/index.html b/main_dc/valitovgaziz/html/index.html index 1b8babc..5bbe69e 100644 --- a/main_dc/valitovgaziz/html/index.html +++ b/main_dc/valitovgaziz/html/index.html @@ -15,53 +15,10 @@ + ValitovGaziz - Предприниматель - Fullstack-разработчик - - - - - -