RRWeb 测试录制工具

使用rrweb录制网页操作,方便测试同学快速定位问题

// ==UserScript==
// @name         RRWeb 测试录制工具
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  使用rrweb录制网页操作,方便测试同学快速定位问题
// @author       RRWeb Recorder Team
// @match        *://*/*
// @grant        none
// @license MIT
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/rrweb.min.js
// @supportURL   https://github.com/your-username/rrweb-recorder/issues
// @homepageURL  https://github.com/your-username/rrweb-recorder
// ==/UserScript==

(function () {
    'use strict';

    // 录制相关变量
    let isRecording = false;
    let stopRecording = null;
    let events = [];
    let startTime = null;

    // 创建悬浮按钮
    function createFloatingButton() {
        const button = document.createElement('div');
        button.id = 'rrweb-recorder-btn';
        button.innerHTML = `
            <div class="recorder-icon">
                <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
                    <circle cx="12" cy="12" r="8"/>
                </svg>
            </div>
            <span class="recorder-text">开始录制</span>
        `;

        // 样式
        button.style.cssText = `
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 120px;
            height: 50px;
            background: #4CAF50;
            color: white;
            border-radius: 25px;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            z-index: 10000;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            font-size: 14px;
            font-weight: 500;
            transition: all 0.3s ease;
            user-select: none;
            gap: 8px;
        `;

        // 悬停效果
        button.addEventListener('mouseenter', () => {
            button.style.transform = 'scale(1.05)';
            button.style.boxShadow = '0 6px 16px rgba(0,0,0,0.2)';
        });

        button.addEventListener('mouseleave', () => {
            button.style.transform = 'scale(1)';
            button.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
        });

        // 点击事件
        button.addEventListener('click', toggleRecording);

        document.body.appendChild(button);
        return button;
    }

    // 切换录制状态
    function toggleRecording() {
        if (isRecording) {
            stopRecordingSession();
        } else {
            startRecordingSession();
        }
    }

    // 开始录制
    function startRecordingSession() {
        if (typeof rrweb === 'undefined') {
            showNotification('rrweb库加载失败,请刷新页面重试', 'error');
            return;
        }

        events = [];
        startTime = Date.now();
        isRecording = true;

        try {
            stopRecording = rrweb.record({
                emit(event) {
                    events.push(event);
                },
                checkoutEveryNms: 10 * 1000, // 每10秒创建一个检查点
                maskTextSelector: '[data-sensitive]', // 遮蔽敏感文本
                maskInputOptions: {
                    password: true,
                    email: false,
                    text: false
                }
            });

            updateButtonState();
            showNotification('开始录制...', 'success');
        } catch (error) {
            console.error('录制启动失败:', error);
            showNotification('录制启动失败: ' + error.message, 'error');
            isRecording = false;
        }
    }

    // 停止录制
    function stopRecordingSession() {
        if (stopRecording) {
            stopRecording();
            stopRecording = null;
        }

        isRecording = false;
        updateButtonState();

        if (events.length > 0) {
            showNotification('录制完成,准备下载...', 'success');
            downloadRecording();
        } else {
            showNotification('没有录制到任何事件', 'warning');
        }
    }

    // 更新按钮状态
    function updateButtonState() {
        const button = document.getElementById('rrweb-recorder-btn');
        const icon = button.querySelector('.recorder-icon svg circle');
        const text = button.querySelector('.recorder-text');

        if (isRecording) {
            button.style.background = '#f44336';
            text.textContent = '停止录制';
            icon.style.animation = 'pulse 1.5s infinite';

            // 添加脉冲动画
            if (!document.getElementById('rrweb-pulse-style')) {
                const style = document.createElement('style');
                style.id = 'rrweb-pulse-style';
                style.textContent = `
                    @keyframes pulse {
                        0% { opacity: 1; }
                        50% { opacity: 0.5; }
                        100% { opacity: 1; }
                    }
                `;
                document.head.appendChild(style);
            }
        } else {
            button.style.background = '#4CAF50';
            text.textContent = '开始录制';
            icon.style.animation = 'none';
        }
    }

    // 下载录制文件
    function downloadRecording() {
        const recordingData = {
            events: events,
            startTime: startTime,
            endTime: Date.now(),
            url: window.location.href,
            userAgent: navigator.userAgent,
            timestamp: new Date().toISOString()
        };

        const dataStr = JSON.stringify(recordingData, null, 2);
        const dataBlob = new Blob([dataStr], { type: 'application/json' });

        const link = document.createElement('a');
        link.href = URL.createObjectURL(dataBlob);
        link.download = `recording-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;

        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);

        URL.revokeObjectURL(link.href);
    }

    // 显示通知
    function showNotification(message, type = 'info') {
        const notification = document.createElement('div');
        notification.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 12px 20px;
            border-radius: 8px;
            color: white;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            font-size: 14px;
            z-index: 10001;
            max-width: 300px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            transition: all 0.3s ease;
        `;

        // 根据类型设置颜色
        const colors = {
            success: '#4CAF50',
            error: '#f44336',
            warning: '#ff9800',
            info: '#2196F3'
        };
        notification.style.background = colors[type] || colors.info;
        notification.textContent = message;

        document.body.appendChild(notification);

        // 3秒后自动移除
        setTimeout(() => {
            if (notification.parentNode) {
                notification.style.opacity = '0';
                notification.style.transform = 'translateX(100%)';
                setTimeout(() => {
                    document.body.removeChild(notification);
                }, 300);
            }
        }, 3000);
    }

    // 等待页面加载完成后初始化
    function init() {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', init);
            return;
        }

        // 检查rrweb是否加载
        if (typeof rrweb === 'undefined') {
            console.warn('rrweb库未加载,等待加载...');
            setTimeout(init, 1000);
            return;
        }

        createFloatingButton();
        console.log('RRWeb录制工具已加载');
    }

    // 启动初始化
    init();

    // 页面卸载时停止录制
    window.addEventListener('beforeunload', () => {
        if (isRecording) {
            stopRecordingSession();
        }
    });

})();