Last active
April 10, 2026 21:44
-
-
Save ggorlen/81a50ac3625e80a544a3a651cb8d60aa to your computer and use it in GitHub Desktop.
HAR minimizer.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>HAR Cleaner</title> | |
| <style> | |
| :root { color-scheme: light dark; } | |
| body { margin:0; font-family: system-ui, sans-serif; } | |
| #overlay { | |
| position: fixed; inset:0; display:flex; align-items:center; justify-content:center; | |
| z-index: 10; | |
| } | |
| #dropZone { | |
| border:1px dashed currentColor; | |
| padding:20px; | |
| max-width: 420px; | |
| background: Canvas; | |
| } | |
| .opts { font-size:12px; margin-top:12px; } | |
| textarea { | |
| position: fixed; inset:0; width:100%; height:100%; | |
| border:none; outline:none; resize:none; | |
| font-family: monospace; font-size:12px; | |
| padding:16px; box-sizing:border-box; | |
| display: none; | |
| } | |
| #copyBtn, #stats { | |
| position: fixed; | |
| top:12px; | |
| padding:6px 10px; | |
| border:1px solid currentColor; | |
| background: Canvas; | |
| font-size:12px; | |
| z-index: 20; | |
| } | |
| #copyBtn { right:12px; cursor:pointer; } | |
| #stats { left:12px; } | |
| #copyBtn.hidden, #stats.hidden { display:none; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="overlay"> | |
| <div id="dropZone"> | |
| <div><strong>Drop HAR file to extract API calls</strong></div> | |
| <div class="opts"> | |
| <div><strong>Preset</strong></div> | |
| <select id="preset"> | |
| <option value="api">API Only</option> | |
| <option value="text">All Text</option> | |
| <option value="all">Everything</option> | |
| </select> | |
| </div> | |
| <div class="opts"> | |
| <div><strong>Content Types</strong></div> | |
| <label><input type="checkbox" value="json" checked> application/json</label><br> | |
| <label><input type="checkbox" value="js"> application/javascript</label><br> | |
| <label><input type="checkbox" value="html" checked> text/html</label><br> | |
| <label><input type="checkbox" value="css"> text/css</label><br> | |
| <label><input type="checkbox" value="image"> images</label><br> | |
| <label><input type="checkbox" value="font"> fonts</label><br> | |
| </div> | |
| <div class="opts"> | |
| <label><input type="checkbox" id="trim" checked> Trim large responses</label><br> | |
| <label><input type="checkbox" id="headers" checked> Remove tracking headers</label><br> | |
| <label><input type="checkbox" id="cookies" checked> Remove cookies</label> | |
| </div> | |
| </div> | |
| </div> | |
| <textarea id="output"></textarea> | |
| <div id="stats" class="hidden"></div> | |
| <button id="copyBtn" class="hidden">Copy</button> | |
| <script> | |
| // similar to: https://tweaksuite.com/tools/har-editor | |
| const output = document.getElementById('output'); | |
| const overlay = document.getElementById('overlay'); | |
| const copyBtn = document.getElementById('copyBtn'); | |
| const stats = document.getElementById('stats'); | |
| const preset = document.getElementById('preset'); | |
| const typeChecks = Array.from(document.querySelectorAll('input[type="checkbox"][value]')); | |
| const opts = { | |
| trim: document.getElementById('trim'), | |
| headers: document.getElementById('headers'), | |
| cookies: document.getElementById('cookies') | |
| }; | |
| let lastMinified = ''; | |
| const applyPreset = (value) => { | |
| const map = { | |
| api: { json: true, html: false, js: false, css: false, image: false, font: false }, | |
| text: { json: true, html: true, js: true, css: true, image: false, font: false }, | |
| all: { json: true, html: true, js: true, css: true, image: true, font: true } | |
| }; | |
| const config = map[value]; | |
| typeChecks.forEach(cb => cb.checked = !!config[cb.value]); | |
| }; | |
| preset.addEventListener('change', (e) => applyPreset(e.target.value)); | |
| applyPreset('api'); | |
| const isAllowedType = (mime) => { | |
| const selected = new Set(typeChecks.filter(c => c.checked).map(c => c.value)); | |
| if (mime.includes('json')) return selected.has('json'); | |
| if (mime.includes('javascript')) return selected.has('js'); | |
| if (mime.includes('html')) return selected.has('html'); | |
| if (mime.includes('css')) return selected.has('css'); | |
| if (mime.startsWith('image/')) return selected.has('image'); | |
| if (mime.includes('font')) return selected.has('font'); | |
| return false; | |
| }; | |
| const isUseful = (entry) => { | |
| const mime = entry.response?.content?.mimeType || ''; | |
| const method = entry.request?.method || ''; | |
| if (method !== 'GET') return true; | |
| return isAllowedType(mime); | |
| }; | |
| const cleanHeaders = (headers) => { | |
| return headers.filter(h => { | |
| const name = h.name.toLowerCase(); | |
| if (opts.cookies.checked && (name === 'cookie' || name === 'set-cookie')) return false; | |
| if (opts.headers.checked && (name.startsWith('x-') || name.startsWith('cf-') || name.includes('cloudflare'))) return false; | |
| return true; | |
| }); | |
| }; | |
| const trimText = (text) => { | |
| if (!opts.trim.checked || !text) return text; | |
| if (text.length <= 500) return text; | |
| return text.slice(0, 500) + '\n[...excluded for brevity]'; | |
| }; | |
| const minimizeEntry = (entry) => ({ | |
| url: entry.request.url, | |
| method: entry.request.method, | |
| status: entry.response.status, | |
| mime: entry.response?.content?.mimeType, | |
| requestHeaders: cleanHeaders(entry.request.headers || []), | |
| responseHeaders: cleanHeaders(entry.response.headers || []), | |
| postData: trimText(entry.request.postData?.text), | |
| response: trimText(entry.response.content?.text) | |
| }); | |
| const formatBytes = (bytes) => { | |
| if (bytes < 1024) return bytes + ' B'; | |
| if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; | |
| return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; | |
| }; | |
| const handleFile = (file) => { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| try { | |
| const har = JSON.parse(e.target.result); | |
| const cleaned = har.log.entries.filter(isUseful).map(minimizeEntry); | |
| const pretty = JSON.stringify(cleaned, null, 2); | |
| const minified = JSON.stringify(cleaned); | |
| lastMinified = minified; | |
| output.value = pretty; | |
| output.style.display = 'block'; | |
| stats.textContent = `${cleaned.length} entries + ${formatBytes(minified.length)}`; | |
| stats.classList.remove('hidden'); | |
| overlay.style.display = 'none'; | |
| copyBtn.classList.remove('hidden'); | |
| } catch { | |
| alert('Invalid HAR file'); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| }; | |
| window.addEventListener('dragover', (e) => e.preventDefault()); | |
| window.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| const file = e.dataTransfer.files[0]; | |
| if (file?.name.endsWith('.har')) handleFile(file); | |
| }); | |
| copyBtn.addEventListener('click', async () => { | |
| await navigator.clipboard.writeText(lastMinified); | |
| copyBtn.textContent = 'Copied'; | |
| setTimeout(() => copyBtn.textContent = 'Copy', 1000); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment