本文目录导读:

我来详细解释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等。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播