JSON Response Capture

Capture and save JSON responses from web requests with URL filtering

Versión del día 19/02/2025. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         JSON Response Capture
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Capture and save JSON responses from web requests with URL filtering
// @author       nickm8
// @license      MIT
// @match        *://*/*
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js
// ==/UserScript==
 

(function() {
    'use strict';

    const style = document.createElement('style');
    style.textContent = `
        :root {
            --bg-primary: #ffffff;
            --bg-secondary: #f9fafb;
            --text-primary: #1f2937;
            --text-secondary: #4b5563;
            --text-tertiary: #6b7280;
            --border-color: #e5e7eb;
            --shadow-color: rgba(0, 0, 0, 0.1);
            --hover-color: #2563eb;
            --danger-color: #dc2626;
            --tag-bg: #f3f4f6;
        }

        @media (prefers-color-scheme: dark) {
            :root {
                --bg-primary: #1f2937;
                --bg-secondary: #374151;
                --text-primary: #f9fafb;
                --text-secondary: #d1d5db;
                --text-tertiary: #9ca3af;
                --border-color: #4b5563;
                --shadow-color: rgba(0, 0, 0, 0.3);
                --hover-color: #60a5fa;
                --danger-color: #ef4444;
                --tag-bg: #374151;
            }
        }

        .json-capture-panel {
            position: fixed;
            bottom: 1rem;
            right: 1rem;
            background: var(--bg-primary);
            color: var(--text-primary);
            border-radius: 0.5rem;
            box-shadow: 0 4px 6px -1px var(--shadow-color);
            transition: all 200ms;
            z-index: 10000;
        }

        .json-capture-panel.minimized { width: 3rem; }
        .json-capture-panel.expanded { width: 24rem; }

        .json-capture-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 0.5rem;
            border-bottom: 1px solid var(--border-color);
            background: var(--bg-secondary);
            border-top-left-radius: 0.5rem;
            border-top-right-radius: 0.5rem;
        }

        .json-capture-content {
            max-height: 24rem;
            overflow: auto;
            padding: 1rem;
        }

        .json-capture-item {
            margin-bottom: 1rem;
            padding: 0.5rem;
            border: 1px solid var(--border-color);
            border-radius: 0.375rem;
        }

        .json-capture-url {
            font-size: 0.875rem;
            color: var(--text-secondary);
            margin-bottom: 0.25rem;
            word-break: break-all;
        }

        .json-capture-timestamp {
            font-size: 0.75rem;
            color: var(--text-tertiary);
            margin-bottom: 0.5rem;
        }

        .json-capture-json {
            font-size: 0.75rem;
            background: var(--bg-secondary);
            padding: 0.5rem;
            border-radius: 0.375rem;
            overflow: auto;
            white-space: pre-wrap;
            max-height: 12rem;
            color: var(--text-primary);
        }

        .json-capture-button {
            padding: 0.25rem 0.5rem;
            background: none;
            border: none;
            cursor: pointer;
            color: var(--text-secondary);
        }

        .json-capture-button:hover {
            color: var(--hover-color);
        }

        .json-capture-button.delete:hover {
            color: var(--danger-color);
        }

        .config-section {
            margin-bottom: 1rem;
            padding: 0.5rem;
            border: 1px solid var(--border-color);
            border-radius: 0.375rem;
        }

        .config-title {
            font-weight: 600;
            margin-bottom: 0.5rem;
            color: var(--text-primary);
        }

        .config-input {
            display: flex;
            gap: 0.5rem;
            margin-bottom: 0.5rem;
        }

        .config-input input {
            flex: 1;
            padding: 0.25rem 0.5rem;
            border: 1px solid var(--border-color);
            border-radius: 0.25rem;
            background: var(--bg-primary);
            color: var(--text-primary);
        }

        .config-list {
            display: flex;
            flex-wrap: wrap;
            gap: 0.25rem;
        }

        .config-tag {
            background: var(--tag-bg);
            color: var(--text-primary);
            padding: 0.25rem 0.5rem;
            border-radius: 0.25rem;
            display: flex;
            align-items: center;
            gap: 0.25rem;
        }

        .config-tag button {
            padding: 0;
            background: none;
            border: none;
            cursor: pointer;
            color: var(--text-tertiary);
        }

        .tabs {
            display: flex;
            border-bottom: 1px solid var(--border-color);
            margin-bottom: 1rem;
        }

        .tab {
            padding: 0.5rem 1rem;
            cursor: pointer;
            border-bottom: 2px solid transparent;
            color: var(--text-secondary);
        }

        .tab.active {
            border-bottom-color: var(--hover-color);
            color: var(--hover-color);
        }
    `;
    document.head.appendChild(style);
 
    const { useState, useEffect } = React;
 
    // Configuration component
    function ConfigSection({ matches, ignores, onUpdateMatches, onUpdateIgnores }) {
        const [newMatch, setNewMatch] = useState('');
        const [newIgnore, setNewIgnore] = useState('');
 
        const addMatch = () => {
            if (newMatch && !matches.includes(newMatch)) {
                onUpdateMatches([...matches, newMatch]);
                setNewMatch('');
            }
        };
 
        const addIgnore = () => {
            if (newIgnore && !ignores.includes(newIgnore)) {
                onUpdateIgnores([...ignores, newIgnore]);
                setNewIgnore('');
            }
        };
 
        const removeMatch = (match) => {
            onUpdateMatches(matches.filter(m => m !== match));
        };
 
        const removeIgnore = (ignore) => {
            onUpdateIgnores(ignores.filter(i => i !== ignore));
        };
 
        return React.createElement('div', { className: 'config-content' }, [
            React.createElement('div', { key: 'matches', className: 'config-section' }, [
                React.createElement('div', { className: 'config-title' }, 'URL Matches'),
                React.createElement('div', { className: 'config-input' }, [
                    React.createElement('input', {
                        value: newMatch,
                        onChange: (e) => setNewMatch(e.target.value),
                        placeholder: 'Enter URL keyword to match',
                        onKeyPress: (e) => e.key === 'Enter' && addMatch()
                    }),
                    React.createElement('button', {
                        className: 'json-capture-button',
                        onClick: addMatch
                    }, '+')
                ]),
                React.createElement('div', { className: 'config-list' },
                    matches.map(match => 
                        React.createElement('span', { key: match, className: 'config-tag' }, [
                            match,
                            React.createElement('button', {
                                onClick: () => removeMatch(match)
                            }, '×')
                        ])
                    )
                )
            ]),
            React.createElement('div', { key: 'ignores', className: 'config-section' }, [
                React.createElement('div', { className: 'config-title' }, 'URL Ignores'),
                React.createElement('div', { className: 'config-input' }, [
                    React.createElement('input', {
                        value: newIgnore,
                        onChange: (e) => setNewIgnore(e.target.value),
                        placeholder: 'Enter URL keyword to ignore',
                        onKeyPress: (e) => e.key === 'Enter' && addIgnore()
                    }),
                    React.createElement('button', {
                        className: 'json-capture-button',
                        onClick: addIgnore
                    }, '+')
                ]),
                React.createElement('div', { className: 'config-list' },
                    ignores.map(ignore => 
                        React.createElement('span', { key: ignore, className: 'config-tag' }, [
                            ignore,
                            React.createElement('button', {
                                onClick: () => removeIgnore(ignore)
                            }, '×')
                        ])
                    )
                )
            ])
        ]);
    }
 
    function JsonCapturePanel() {
        const [captures, setCaptures] = useState([]);
        const [isMinimized, setIsMinimized] = useState(true);
        const [activeTab, setActiveTab] = useState('captures');
        const [matches, setMatches] = useState([]);
        const [ignores, setIgnores] = useState([]);
        const domain = window.location.hostname;
 
        // Load configuration from localStorage
        useEffect(() => {
            const config = JSON.parse(localStorage.getItem(`jsonCapture_${domain}`) || '{"matches":[],"ignores":[]}');
            setMatches(config.matches);
            setIgnores(config.ignores);
        }, []);
 
        // Save configuration to localStorage
        useEffect(() => {
            localStorage.setItem(`jsonCapture_${domain}`, JSON.stringify({ matches, ignores }));
        }, [matches, ignores]);
 
        const shouldCaptureUrl = (url) => {
            if (matches.length === 0) return false;
            return matches.some(match => url.includes(match)) && 
                   !ignores.some(ignore => url.includes(ignore));
        };
 
        useEffect(() => {
            // Intercept fetch
            const originalFetch = window.fetch;
            window.fetch = async function(...args) {
                const response = await originalFetch.apply(this, args);
                const clone = response.clone();
 
                try {
                    const contentType = clone.headers.get('content-type');
                    if (contentType && contentType.includes('application/json')) {
                        const url = typeof args[0] === 'string' ? args[0] : args[0].url;
                        if (shouldCaptureUrl(url)) {
                            const json = await clone.json();
                            setCaptures(prev => [...prev, {
                                timestamp: new Date().toISOString(),
                                url,
                                data: json
                            }]);
                        }
                    }
                } catch (err) {
                    console.error('Error processing response:', err);
                }
 
                return response;
            };
 
            // Intercept XHR
            const originalXHROpen = XMLHttpRequest.prototype.open;
            const originalXHRSend = XMLHttpRequest.prototype.send;
 
            XMLHttpRequest.prototype.open = function(...args) {
                this._url = args[1];
                return originalXHROpen.apply(this, args);
            };
 
            XMLHttpRequest.prototype.send = function(...args) {
                this.addEventListener('load', function() {
                    try {
                        const contentType = this.getResponseHeader('content-type');
                        if (contentType && contentType.includes('application/json')) {
                            if (shouldCaptureUrl(this._url)) {
                                const json = JSON.parse(this.responseText);
                                setCaptures(prev => [...prev, {
                                    timestamp: new Date().toISOString(),
                                    url: this._url,
                                    data: json
                                }]);
                            }
                        }
                    } catch (err) {
                        console.error('Error processing XHR response:', err);
                    }
                });
 
                return originalXHRSend.apply(this, args);
            };
        }, [matches, ignores]);
 
        const handleSave = () => {
            const blob = new Blob([JSON.stringify(captures, null, 2)], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `json-captures-${domain}-${new Date().toISOString()}.json`;
            a.click();
            URL.revokeObjectURL(url);
        };
 
        const handleClear = () => {
            setCaptures([]);
        };
 
        return React.createElement('div', {
            className: `json-capture-panel ${isMinimized ? 'minimized' : 'expanded'}`
        }, [
            // Header
            React.createElement('div', {
                key: 'header',
                className: 'json-capture-header'
            }, [
                !isMinimized && React.createElement('div', {
                    key: 'title'
                }, `JSON Captures (${captures.length})`),
                React.createElement('div', {
                    key: 'controls',
                    style: { display: 'flex', gap: '0.5rem' }
                }, [
                    !isMinimized && React.createElement('button', {
                        key: 'save',
                        className: 'json-capture-button',
                        onClick: handleSave,
                        title: 'Save captures'
                    }, '💾'),
                    React.createElement('button', {
                        key: 'toggle',
                        className: 'json-capture-button',
                        onClick: () => setIsMinimized(!isMinimized),
                        title: isMinimized ? 'Expand' : 'Minimize'
                    }, isMinimized ? '⤢' : '⤡'),
                    !isMinimized && React.createElement('button', {
                        key: 'clear',
                        className: 'json-capture-button delete',
                        onClick: handleClear,
                        title: 'Clear captures'
                    }, '✕')
                ])
            ]),
            // Content
            !isMinimized && React.createElement('div', { key: 'content' }, [
                // Tabs
                React.createElement('div', { key: 'tabs', className: 'tabs' }, [
                    React.createElement('div', {
                        className: `tab ${activeTab === 'captures' ? 'active' : ''}`,
                        onClick: () => setActiveTab('captures')
                    }, 'Captures'),
                    React.createElement('div', {
                        className: `tab ${activeTab === 'config' ? 'active' : ''}`,
                        onClick: () => setActiveTab('config')
                    }, 'Configuration')
                ]),
                // Tab content
                activeTab === 'captures' ? 
                    React.createElement('div', {
                        key: 'captures',
                        className: 'json-capture-content'
                    },
                        captures.length === 0
                            ? React.createElement('div', {
                                style: { textAlign: 'center', color: '#6b7280' }
                            }, matches.length === 0 ? 'Configure URL matches to start capturing' : 'No JSON responses captured yet')
                            : captures.map((capture, index) =>
                                React.createElement('div', {
                                    key: index,
                                    className: 'json-capture-item'
                                }, [
                                    React.createElement('div', {
                                        key: 'url',
                                        className: 'json-capture-url'
                                    }, capture.url),
                                    React.createElement('div', {
                                        key: 'timestamp',
                                        className: 'json-capture-timestamp'
                                    }, capture.timestamp),
                                    React.createElement('pre', {
                                        key: 'json',
                                        className: 'json-capture-json'
                                    }, JSON.stringify(capture.data, null, 2))
                                ])
                            )
                    )
                    : React.createElement(ConfigSection, {
                        key: 'config',
                        matches,
                        ignores,
                        onUpdateMatches: setMatches,
                        onUpdateIgnores: setIgnores
                    })
            ])
]);
}
 
const container = document.createElement('div');
document.body.appendChild(container);
ReactDOM.render(React.createElement(JsonCapturePanel), container);
})();