JSON Response Capture

Capture and save JSON responses from web requests with URL filtering

От 19.02.2025. Виж последната версия.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==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);
})();