Matomo Heartbeat May 2026

private function endSession($sessionId) $stmt = $this->db->prepare(" UPDATE matomo_heartbeat_sessions SET is_active = 0 WHERE id = ? "); $stmt->execute([$sessionId]);

stopHeartbeat() if (this.intervalId) clearInterval(this.intervalId); this.intervalId = null; // Calculate engaged time for this session if (this.lastHeartbeatTime) const engagedTime = Math.floor((Date.now() - this.lastHeartbeatTime) / 1000); if (engagedTime >= this.options.minVisitLength) this.totalEngagedTime += engagedTime; this.isActive = false; this.log('Heartbeat stopped');

$data = json_decode(file_get_contents('php://input'), true); $result = $tracker->recordHeartbeat( session_id(), $data['visitor_id'] ?? '', $data['page_url'] ?? '', $data['engaged_time'] ?? 0 ); matomo heartbeat

init() this.bindEvents(); this.startIdleMonitoring(); this.log('Matomo Heartbeat initialized');

sendBeacon(data) // Use sendBeacon for reliable final tracking if (navigator.sendBeacon) const formData = new FormData(); Object.keys(data).forEach(key => formData.append(key, data[key]); ); navigator.sendBeacon('/matomo.php', formData); '', $data['engaged_time']

public function getEngagementReport($dateRange = 'last30') $apiUrl = $this->matomoUrl . '/index.php'; $params = [ 'module' => 'API', 'method' => 'CustomEvents.getCustomEvent', 'idSite' => $this->siteId, 'period' => 'range', 'date' => $dateRange, 'format' => 'json', 'filter_limit' => -1 ]; if ($this->tokenAuth) $params['token_auth'] = $this->tokenAuth; $url = $apiUrl . '?' . http_build_query($params); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); curl_close($ch); return json_decode($response, true);

public function getEngagementMetrics($dateFrom, $dateTo) $stmt = $this->db->prepare(" SELECT COUNT(DISTINCT session_id) as total_sessions, SUM(total_engaged_time) as total_engagement_seconds, AVG(total_engaged_time) as avg_engagement_seconds, AVG(heartbeat_count) as avg_heartbeats_per_session, COUNT(CASE WHEN total_engaged_time > 60 THEN 1 END) as long_sessions, COUNT(CASE WHEN total_engaged_time < 10 THEN 1 END) as bounce_sessions FROM matomo_heartbeat_sessions WHERE start_time BETWEEN ? AND ? AND is_active = 0 "); $stmt->execute([$dateFrom, $dateTo]); $metrics = $stmt->fetch(PDO::FETCH_ASSOC); if ($metrics['total_sessions'] > 0) $metrics['bounce_rate'] = ($metrics['bounce_sessions'] / $metrics['total_sessions']) * 100; $metrics['engagement_rate'] = ($metrics['long_sessions'] / $metrics['total_sessions']) * 100; return $metrics; AND is_active = 0 ")

sendHeartbeat(type) const now = Date.now(); const timeSinceLastHeartbeat = (now - this.lastHeartbeatTime) / 1000; // Don't send heartbeat if too short if (timeSinceLastHeartbeat < this.options.minVisitLength && type === 'ongoing') return; const heartbeatData = action_name: 'Heartbeat', e_c: 'Engagement', e_a: type, e_n: 'User Activity', e_v: Math.floor(timeSinceLastHeartbeat), _cvar: JSON.stringify( heartbeat_interval: this.options.heartbeatInterval, time_on_page: Math.floor((now - this.visitStartTime) / 1000), total_engaged_time: this.totalEngagedTime + Math.floor((now - this.lastHeartbeatTime) / 1000) ) ; // Send to Matomo if (window._paq) window._paq.push(['trackEvent', heartbeatData.e_c, heartbeatData.e_a, heartbeatData.e_n, heartbeatData.e_v]); // Optional: track custom variable window._paq.push(['setCustomVariable', 1, 'HeartbeatType', type, 'page']); this.log(`Heartbeat sent: $type`, heartbeatData); this.lastHeartbeatTime = now;