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:
2025-11-11 03:22:14 +05:00
parent ea9540dc73
commit a013fcacd8
7 changed files with 576 additions and 45 deletions
+40 -1
View File
@@ -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:
+17
View File
@@ -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 {
+26
View File
@@ -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"
}
+192
View File
@@ -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 () { };
}
+1 -44
View File
@@ -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">