PHP 怎么PHP 会话录制

wen PHP项目 3

本文目录导读:

PHP 怎么PHP 会话录制

  1. 什么是PHP会话录制
  2. 实现方法
  3. 注意事项

我来详细解释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';
    }
}

性能优化

  1. 批量发送:不要每条事件都发请求
  2. 压缩数据:使用gzip压缩传输
  3. 限制存储:设置最大记录数或自动清理
  4. 异步处理:使用消息队列处理大量数据

这个实现可以满足基本的会话录制需求,实际生产环境中建议使用成熟的解决方案如FullStory、Hotjar等。

抱歉,评论功能暂时关闭!