📋 Что получится
- Метрики обновляются в реальном времени
- Графики без перезагрузки страницы
- Алерты при критических значениях
- Список активных пользователей
Любой бэкенд: Примеры на PHP, но паттерн универсален — работает с Node.js, Python, Go и др.
🏗️ Архитектура
Источник данных
БД, API, IoT
→
Ваш сервер
агрегация данных
→
Pushler.ru
WebSocket
Дашборд
мгновенное обновление
💻 Код сервера
Сервер собирает метрики и отправляет их в реальном времени. Примеры на PHP.
Установка PHP SDK
Bash
composer require pushler/php-sdk
Сервис метрик
PHP
<?php
// MetricsService.php — сбор и отправка метрик
require_once 'vendor/autoload.php';
use PushlerRu\PushlerClient;
class MetricsService
{
private PushlerClient $pushler;
private PDO $pdo;
public function __construct(string $appKey, string $appSecret, PDO $pdo)
{
$this->pushler = new PushlerClient($appKey, $appSecret);
$this->pdo = $pdo;
}
/**
* Собрать и отправить все метрики
*/
public function broadcastMetrics(): void
{
$metrics = $this->collectMetrics();
$this->pushler->trigger(
'dashboard',
'metrics:update',
[
'metrics' => $metrics,
'timestamp' => date('c')
]
);
}
/**
* Сбор метрик из БД
*/
public function collectMetrics(): array
{
return [
'users' => $this->getUsersMetrics(),
'orders' => $this->getOrdersMetrics(),
'revenue' => $this->getRevenueMetrics(),
'performance' => $this->getPerformanceMetrics(),
];
}
private function getUsersMetrics(): array
{
// Онлайн за последние 5 минут
$stmt = $this->pdo->query("
SELECT COUNT(*) as online
FROM users
WHERE last_activity > DATE_SUB(NOW(), INTERVAL 5 MINUTE)
");
$online = $stmt->fetchColumn();
// Новые за сегодня
$stmt = $this->pdo->query("
SELECT COUNT(*) as new_today
FROM users
WHERE DATE(created_at) = CURDATE()
");
$newToday = $stmt->fetchColumn();
// Всего
$stmt = $this->pdo->query("SELECT COUNT(*) FROM users");
$total = $stmt->fetchColumn();
return [
'online' => (int) $online,
'new_today' => (int) $newToday,
'total' => (int) $total,
];
}
private function getOrdersMetrics(): array
{
// Заказы за сегодня
$stmt = $this->pdo->query("
SELECT
COUNT(*) as count,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
SUM(CASE WHEN status = 'processing' THEN 1 ELSE 0 END) as processing,
SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) as delivered
FROM orders
WHERE DATE(created_at) = CURDATE()
");
return $stmt->fetch(PDO::FETCH_ASSOC);
}
private function getRevenueMetrics(): array
{
// Выручка за сегодня
$stmt = $this->pdo->query("
SELECT COALESCE(SUM(total), 0) as today
FROM orders
WHERE DATE(created_at) = CURDATE() AND status != 'cancelled'
");
$today = $stmt->fetchColumn();
// За вчера (для сравнения)
$stmt = $this->pdo->query("
SELECT COALESCE(SUM(total), 0) as yesterday
FROM orders
WHERE DATE(created_at) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)
AND status != 'cancelled'
");
$yesterday = $stmt->fetchColumn();
$change = $yesterday > 0
? round(($today - $yesterday) / $yesterday * 100, 1)
: 0;
return [
'today' => (float) $today,
'yesterday' => (float) $yesterday,
'change_percent' => $change,
];
}
private function getPerformanceMetrics(): array
{
return [
'cpu' => sys_getloadavg()[0] ?? 0,
'memory' => memory_get_usage(true) / 1024 / 1024,
'requests_per_minute' => $this->getRequestsPerMinute(),
];
}
private function getRequestsPerMinute(): int
{
// Можно брать из Redis, логов или APM
return rand(100, 500); // Заглушка
}
/**
* Отправить алерт при критическом значении
*/
public function sendAlert(string $type, string $message, array $data = []): void
{
$this->pushler->trigger(
'dashboard',
'alert',
[
'type' => $type, // 'warning', 'error', 'critical'
'message' => $message,
'data' => $data,
'timestamp' => date('c')
]
);
}
}
// Использование (cron каждую минуту)
$metrics = new MetricsService(
'key_ваш_ключ',
'secret_ваш_секрет',
getDbConnection()
);
$metrics->broadcastMetrics();
Данные для графиков (история)
PHP
<?php
// chart_data.php — данные для графика за последние 24 часа
require_once 'vendor/autoload.php';
use PushlerRu\PushlerClient;
$pushler = new PushlerClient('key_ваш_ключ', 'secret_ваш_секрет');
$pdo = getDbConnection();
// Заказы по часам за последние 24 часа
$stmt = $pdo->query("
SELECT
DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') as hour,
COUNT(*) as orders,
SUM(total) as revenue
FROM orders
WHERE created_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)
GROUP BY hour
ORDER BY hour
");
$chartData = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Преобразуем для Chart.js
$labels = [];
$ordersData = [];
$revenueData = [];
foreach ($chartData as $row) {
$labels[] = date('H:i', strtotime($row['hour']));
$ordersData[] = (int) $row['orders'];
$revenueData[] = (float) $row['revenue'];
}
// Отправляем обновление графика
$pushler->trigger('dashboard', 'chart:update', [
'chart_id' => 'orders-24h',
'labels' => $labels,
'datasets' => [
['label' => 'Заказы', 'data' => $ordersData],
['label' => 'Выручка', 'data' => $revenueData],
]
]);
Отправка точки данных при новом заказе
PHP
<?php
// При создании заказа — отправляем точку на график
$pushler->trigger('dashboard', 'chart:add-point', [
'chart_id' => 'orders-realtime',
'timestamp' => date('c'),
'value' => $order['total']
]);
// Обновляем счётчики
$pushler->trigger('dashboard', 'counter:increment', [
'counter_id' => 'orders-today',
'increment' => 1
]);
$pushler->trigger('dashboard', 'counter:increment', [
'counter_id' => 'revenue-today',
'increment' => $order['total']
]);
🌐 Код клиента
Vanilla JavaScript с Chart.js
HTML
<!-- Подключение библиотек -->
<script src="https://cdn.pushler.ru/sdk/pushler.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
JavaScript
// Live Dashboard
class LiveDashboard {
constructor(appKey) {
this.appKey = appKey;
this.pushler = null;
this.channel = null;
this.charts = new Map();
this.counters = new Map();
}
async connect() {
this.pushler = new PushlerClient({
appKey: this.appKey
});
return new Promise((resolve) => {
this.pushler.on('connected', () => {
this.channel = this.pushler.subscribe('dashboard');
this.setupListeners();
resolve();
});
});
}
setupListeners() {
// Обновление всех метрик
this.channel.on('metrics:update', (data) => {
this.updateMetrics(data.metrics);
});
// Обновление графика
this.channel.on('chart:update', (data) => {
this.updateChart(data.chart_id, data);
});
// Добавление точки на график
this.channel.on('chart:add-point', (data) => {
this.addChartPoint(data.chart_id, data.timestamp, data.value);
});
// Инкремент счётчика
this.channel.on('counter:increment', (data) => {
this.incrementCounter(data.counter_id, data.increment);
});
// Алерты
this.channel.on('alert', (data) => {
this.showAlert(data);
});
}
updateMetrics(metrics) {
// Пользователи
if (metrics.users) {
this.animateValue('users-online', metrics.users.online);
this.animateValue('users-new', metrics.users.new_today);
this.animateValue('users-total', metrics.users.total);
}
// Заказы
if (metrics.orders) {
this.animateValue('orders-today', metrics.orders.count);
this.animateValue('orders-pending', metrics.orders.pending);
}
// Выручка
if (metrics.revenue) {
this.animateValue('revenue-today', metrics.revenue.today, true);
this.updateChangeIndicator('revenue-change', metrics.revenue.change_percent);
}
}
animateValue(elementId, endValue, isCurrency = false) {
const el = document.getElementById(elementId);
if (!el) return;
const startValue = parseFloat(el.dataset.value || 0);
const duration = 500;
const startTime = performance.now();
const animate = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Easing
const eased = 1 - Math.pow(1 - progress, 3);
const current = startValue + (endValue - startValue) * eased;
el.textContent = isCurrency
? this.formatCurrency(current)
: this.formatNumber(current);
el.dataset.value = endValue;
if (progress < 1) {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
}
formatNumber(num) {
return new Intl.NumberFormat('ru-RU').format(Math.round(num));
}
formatCurrency(num) {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
maximumFractionDigits: 0
}).format(num);
}
updateChangeIndicator(elementId, percent) {
const el = document.getElementById(elementId);
if (!el) return;
el.textContent = (percent >= 0 ? '+' : '') + percent + '%';
el.className = 'change ' + (percent >= 0 ? 'positive' : 'negative');
}
// Регистрация Chart.js графика
registerChart(chartId, chartInstance) {
this.charts.set(chartId, chartInstance);
}
updateChart(chartId, data) {
const chart = this.charts.get(chartId);
if (!chart) return;
chart.data.labels = data.labels;
data.datasets.forEach((ds, i) => {
if (chart.data.datasets[i]) {
chart.data.datasets[i].data = ds.data;
}
});
chart.update('none');
}
addChartPoint(chartId, timestamp, value) {
const chart = this.charts.get(chartId);
if (!chart) return;
const label = new Date(timestamp).toLocaleTimeString('ru-RU', {
hour: '2-digit',
minute: '2-digit'
});
chart.data.labels.push(label);
chart.data.datasets[0].data.push(value);
// Ограничиваем количество точек
if (chart.data.labels.length > 50) {
chart.data.labels.shift();
chart.data.datasets[0].data.shift();
}
chart.update('none');
}
incrementCounter(counterId, increment) {
const el = document.getElementById(counterId);
if (!el) return;
const current = parseFloat(el.dataset.value || 0);
const newValue = current + increment;
this.animateValue(counterId, newValue, counterId.includes('revenue'));
}
showAlert(data) {
const alertsContainer = document.getElementById('alerts');
const alert = document.createElement('div');
alert.className = `alert alert-${data.type}`;
alert.innerHTML = `
<span class="alert-icon">${this.getAlertIcon(data.type)}</span>
<span class="alert-message">${data.message}</span>
<button onclick="this.parentElement.remove()">×</button>
`;
alertsContainer.prepend(alert);
// Звук для critical
if (data.type === 'critical') {
this.playAlertSound();
}
// Автоудаление
setTimeout(() => alert.remove(), 10000);
}
getAlertIcon(type) {
const icons = {
warning: '⚠️',
error: '❌',
critical: '🚨'
};
return icons[type] || '⚠️';
}
playAlertSound() {
const audio = new Audio('/sounds/alert.mp3');
audio.volume = 0.5;
audio.play().catch(() => {});
}
}
// Инициализация
const dashboard = new LiveDashboard('key_ваш_ключ');
dashboard.connect();
// Регистрируем график
const ctx = document.getElementById('ordersChart').getContext('2d');
const ordersChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Заказы',
data: [],
borderColor: '#ff7f1f',
tension: 0.4
}]
},
options: {
responsive: true,
animation: false,
scales: {
y: { beginAtZero: true }
}
}
});
dashboard.registerChart('orders-realtime', ordersChart);
Vue 3 компонент
Vue
<!-- DashboardWidget.vue -->
<template>
<div class="dashboard">
<!-- Алерты -->
<TransitionGroup name="alert" tag="div" class="alerts">
<div v-for="alert in alerts" :key="alert.id" :class="['alert', alert.type]">
{{ alert.message }}
<button @click="removeAlert(alert.id)">×</button>
</div>
</TransitionGroup>
<!-- Метрики -->
<div class="metrics-grid">
<MetricCard
title="Онлайн"
:value="metrics.users?.online || 0"
icon="👥"
/>
<MetricCard
title="Заказов сегодня"
:value="metrics.orders?.count || 0"
icon="📦"
/>
<MetricCard
title="Выручка"
:value="metrics.revenue?.today || 0"
:change="metrics.revenue?.change_percent"
currency
icon="💰"
/>
</div>
<!-- График -->
<div class="chart-container">
<canvas ref="chartEl"></canvas>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { Chart, registerables } from 'chart.js';
import PushlerClient from '@pushler/js';
import MetricCard from './MetricCard.vue';
Chart.register(...registerables);
const props = defineProps<{ appKey: string }>();
const metrics = ref<Record<string, any>>({});
const alerts = ref<Array<{ id: string; type: string; message: string }>>([]);
const chartEl = ref<HTMLCanvasElement | null>(null);
let pushler: PushlerClient | null = null;
let channel: any = null;
let chart: Chart | null = null;
onMounted(() => {
// Инициализация графика
if (chartEl.value) {
chart = new Chart(chartEl.value, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Заказы',
data: [],
borderColor: '#ff7f1f',
backgroundColor: 'rgba(255, 127, 31, 0.1)',
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
animation: false
}
});
}
// Подключение к Pushler
pushler = new PushlerClient({ appKey: props.appKey });
pushler.on('connected', () => {
channel = pushler!.subscribe('dashboard');
channel.on('metrics:update', (data: any) => {
metrics.value = data.metrics;
});
channel.on('chart:add-point', (data: any) => {
if (!chart) return;
const label = new Date(data.timestamp).toLocaleTimeString('ru-RU', {
hour: '2-digit', minute: '2-digit'
});
chart.data.labels!.push(label);
chart.data.datasets[0].data.push(data.value);
if (chart.data.labels!.length > 50) {
chart.data.labels!.shift();
chart.data.datasets[0].data.shift();
}
chart.update('none');
});
channel.on('alert', (data: any) => {
const alert = { id: Date.now().toString(), ...data };
alerts.value.unshift(alert);
setTimeout(() => removeAlert(alert.id), 10000);
});
});
});
onUnmounted(() => {
channel?.unsubscribe();
pushler?.disconnect();
chart?.destroy();
});
function removeAlert(id: string) {
alerts.value = alerts.value.filter(a => a.id !== id);
}
</script>
⚠️ Типичные ошибки
1. Слишком частые обновления
PHP
// ❌ Неправильно — отправляем на каждый запрос
// В обработчике HTTP запроса:
$pushler->trigger('dashboard', 'metrics:update', collectMetrics());
// 1000 RPS = 1000 событий/сек!
// ✅ Правильно — агрегируем и отправляем периодически
// Cron каждую минуту или секунду:
$pushler->trigger('dashboard', 'metrics:update', collectMetrics());
2. Нет анимации — дёргается
JavaScript
// ❌ Неправильно — резкая смена значения
element.textContent = newValue;
// ✅ Правильно — плавная анимация
animateValue(element, oldValue, newValue, 500);
3. График перерисовывается целиком
JavaScript
// ❌ Неправильно — полное обновление графика
chart.data = newData;
chart.update(); // Мигание!
// ✅ Правильно — добавляем точку
chart.data.labels.push(newLabel);
chart.data.datasets[0].data.push(newValue);
chart.update('none'); // Без анимации
Готовы попробовать?
Создайте бесплатный аккаунт и начните интеграцию за пару минут