本文目录导读:

我来详细解释PHP会话录制(Session Recording/Replay)的概念和实现方法。
什么是PHP会话录制
会话录制是指记录用户在网站上的操作行为(点击、输入、页面浏览等),以便后续回放分析,常用于:
- 用户行为分析
- 错误调试
- 用户体验优化
实现方法
基于AJAX的简单实现
// session_recorder.php - 记录用户操作
<?php
session_start();
// 接收前端发送的操作数据
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = json_decode(file_get_contents('php://input'), true);
// 初始化会话录制存储
if (!isset($_SESSION['recording'])) {
$_SESSION['recording'] = [];
}
// 添加操作记录
$_SESSION['recording'][] = [
'timestamp' => microtime(true),
'action' => $action['type'],
'data' => $action['data'] ?? null,
'page' => $action['page'] ?? $_SERVER['HTTP_REFERER'] ?? '',
'session_id' => session_id()
];
// 限制存储大小(防止内存溢出)
if (count($_SESSION['recording']) > 10000) {
array_shift($_SESSION['recording']);
}
echo json_encode(['status' => 'success']);
exit;
}
// 获取录制数据
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['get_recording'])) {
echo json_encode($_SESSION['recording'] ?? []);
exit;
}
?>
前端JavaScript跟踪
// track.js - 前端跟踪脚本
class SessionRecorder {
constructor() {
this.events = [];
this.startTime = Date.now();
this.recordingEndpoint = '/session_recorder.php';
}
init() {
// 跟踪点击事件
document.addEventListener('click', (e) => {
this.recordEvent('click', {
x: e.clientX,
y: e.clientY,
target: e.target.tagName,
text: e.target.innerText?.substring(0, 50)
});
});
// 跟踪表单输入
document.addEventListener('input', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
this.recordEvent('input', {
name: e.target.name,
type: e.target.type,
value: e.target.value
});
}
});
// 跟踪页面滚动
let scrollTimeout;
window.addEventListener('scroll', () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
this.recordEvent('scroll', {
scrollX: window.scrollX,
scrollY: window.scrollY
});
}, 200); // 节流
});
// 定期发送事件
setInterval(() => this.flush(), 5000);
}
recordEvent(type, data) {
this.events.push({
type,
data,
timestamp: Date.now() - this.startTime,
page: window.location.href
});
}
flush() {
if (this.events.length === 0) return;
fetch(this.recordingEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(this.events)
});
this.events = [];
}
}
// 初始化录制
const recorder = new SessionRecorder();
recorder.init();
完整的录制系统
// SessionReplay.php - 完整的录制类
class SessionReplay {
private $db;
private $sessionId;
public function __construct() {
$this->db = new PDO('mysql:host=localhost;dbname=analytics', 'user', 'pass');
$this->sessionId = session_id();
}
// 记录事件到数据库
public function recordEvent($eventData) {
$stmt = $this->db->prepare("
INSERT INTO session_recordings
(session_id, event_type, event_data, page_url, created_at)
VALUES (?, ?, ?, ?, NOW())
");
$stmt->execute([
$this->sessionId,
$eventData['type'],
json_encode($eventData['data']),
$eventData['page_url'] ?? ''
]);
}
// 获取会话录制数据
public function getSessionRecording($sessionId) {
$stmt = $this->db->prepare("
SELECT * FROM session_recordings
WHERE session_id = ?
ORDER BY created_at ASC
");
$stmt->execute([$sessionId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// 回放录制数据
public function replaySession($sessionId) {
$events = $this->getSessionRecording($sessionId);
$replayData = [
'session_id' => $sessionId,
'events' => [],
'duration' => 0
];
foreach ($events as $event) {
$replayData['events'][] = [
'type' => $event['event_type'],
'data' => json_decode($event['event_data'], true),
'timestamp' => strtotime($event['created_at'])
];
}
if (!empty($events)) {
$firstEvent = strtotime($events[0]['created_at']);
$lastEvent = strtotime(end($events)['created_at']);
$replayData['duration'] = $lastEvent - $firstEvent;
}
return $replayData;
}
// 清理过期记录
public function cleanOldRecords($days = 30) {
$stmt = $this->db->prepare("
DELETE FROM session_recordings
WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)
");
$stmt->execute([$days]);
}
}
数据库表结构
-- 会话录制表
CREATE TABLE session_recordings (
id INT AUTO_INCREMENT PRIMARY KEY,
session_id VARCHAR(255) NOT NULL,
user_id INT NULL,
event_type VARCHAR(50) NOT NULL,
event_data JSON,
page_url TEXT,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_session (session_id),
INDEX idx_created (created_at)
);
-- 会话汇总表
CREATE TABLE session_summaries (
id INT AUTO_INCREMENT PRIMARY KEY,
session_id VARCHAR(255) NOT NULL,
user_id INT NULL,
start_time TIMESTAMP,
end_time TIMESTAMP,
page_count INT DEFAULT 0,
click_count INT DEFAULT 0,
input_count INT DEFAULT 0,
scroll_count INT DEFAULT 0,
total_duration INT DEFAULT 0,
UNIQUE KEY uk_session (session_id)
);
客户端实现示例
// 回放播放器
class SessionPlayer {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.events = [];
this.currentIndex = 0;
this.isPlaying = false;
}
loadEvents(events) {
this.events = events;
this.renderTimeline();
}
renderTimeline() {
// 创建时间线UI
this.container.innerHTML = `
<div class="player-controls">
<button onclick="player.play()">▶ 播放</button>
<button onclick="player.pause()">⏸ 暂停</button>
<input type="range" id="timeline" min="0" max="${this.events.length - 1}" value="0">
<span id="eventCount">0 / ${this.events.length}</span>
</div>
<div id="replayArea" style="width: 800px; height: 600px; border: 1px solid #ccc;">
<!-- 回放区域 -->
</div>
`;
}
play() {
if (this.isPlaying || this.events.length === 0) return;
this.isPlaying = true;
this.playNextEvent();
}
playNextEvent() {
if (!this.isPlaying || this.currentIndex >= this.events.length) {
this.isPlaying = false;
return;
}
const event = this.events[this.currentIndex];
this.replayEvent(event);
this.currentIndex++;
// 更新进度
document.getElementById('timeline').value = this.currentIndex;
document.getElementById('eventCount').textContent =
`${this.currentIndex} / ${this.events.length}`;
// 设置下一个事件的延迟
const nextEvent = this.events[this.currentIndex];
const delay = nextEvent ?
(nextEvent.timestamp - event.timestamp) : 1000;
setTimeout(() => this.playNextEvent(), Math.min(delay, 5000));
}
replayEvent(event) {
const replayArea = document.getElementById('replayArea');
switch(event.type) {
case 'click':
// 显示点击位置
const clickMarker = document.createElement('div');
clickMarker.style.cssText = `
position: absolute;
left: ${event.data.x}px;
top: ${event.data.y}px;
width: 10px;
height: 10px;
background: red;
border-radius: 50%;
opacity: 0.5;
`;
replayArea.appendChild(clickMarker);
break;
case 'input':
// 显示输入内容
console.log(`Input on ${event.data.name}: ${event.data.value}`);
break;
}
}
pause() {
this.isPlaying = false;
}
}
// 使用示例
const player = new SessionPlayer('playerContainer');
fetch('/session_recorder.php?get_recording=true')
.then(response => response.json())
.then(events => {
player.loadEvents(events);
});
注意事项
安全考虑
// 安全过滤和验证
class SecureSessionRecording {
public function sanitizeEventData($data) {
// 移除敏感信息
unset($data['password']);
unset($data['credit_card']);
unset($data['token']);
// 限制数据大小
if (strlen(json_encode($data)) > 10240) {
$data = ['error' => 'Data too large'];
}
return $data;
}
public function validateSessionAccess($sessionId) {
// 验证用户是否有权查看该会话
return $_SESSION['user_role'] === 'admin';
}
}
性能优化
- 批量发送:不要每条事件都发请求
- 压缩数据:使用gzip压缩传输
- 限制存储:设置最大记录数或自动清理
- 异步处理:使用消息队列处理大量数据
这个实现可以满足基本的会话录制需求,实际生产环境中建议使用成熟的解决方案如FullStory、Hotjar等。